From aab0f92f382f65439681aa5ddffaa890af7c02b4 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 16 Mar 2026 22:37:34 -0700 Subject: [PATCH 01/23] Add engine integration sections for spatial computing chapters 10-20 Add "Incorporating into the Engine" sections demonstrating practical implementation of Variable Rate Shading, Canted Displays, CAVE Architecture, Warp and Blend, LightField Theory, Plenoptic Synthesis, Scene Understanding, ML Inference, Semantic Occlusion, Platform Divergence, and Spatial Diagnostics CI/CD. Include C++ code examples for renderer_core.cpp, renderer_rendering.cpp, and engine.cpp showing feature enablement, pipeline setup, and compute passes using Vulkan 1.4 and Slang shaders. --- .github/workflows/openxr_engine_ci.yml | 161 + attachments/openxr_engine/CMakeLists.txt | 253 + attachments/openxr_engine/README.adoc | 13 + .../openxr_engine/camera_component.cpp | 142 + attachments/openxr_engine/camera_component.h | 290 ++ attachments/openxr_engine/engine.cpp | 1098 +++++ attachments/openxr_engine/engine.h | 417 ++ .../install_dependencies_linux.sh | 39 + .../install_dependencies_windows.bat | 19 + attachments/openxr_engine/main.cpp | 157 + attachments/openxr_engine/renderer.h | 1932 ++++++++ attachments/openxr_engine/renderer_core.cpp | 1194 +++++ .../openxr_engine/renderer_pipelines.cpp | 1412 ++++++ .../openxr_engine/renderer_rendering.cpp | 2963 ++++++++++++ .../openxr_engine/renderer_resources.cpp | 4074 +++++++++++++++++ .../openxr_engine/shaders/common_types.slang | 205 + .../openxr_engine/shaders/composite.slang | 74 + .../shaders/forward_plus_cull.slang | 136 + attachments/openxr_engine/shaders/hrtf.slang | 262 ++ attachments/openxr_engine/shaders/imgui.slang | 66 + .../openxr_engine/shaders/lighting.slang | 116 + .../shaders/lighting_utils.slang | 119 + attachments/openxr_engine/shaders/pbr.slang | 625 +++ .../openxr_engine/shaders/pbr_utils.slang | 55 + .../openxr_engine/shaders/physics.slang | 460 ++ .../openxr_engine/shaders/ray_query.slang | 1067 +++++ .../openxr_engine/shaders/texturedMesh.slang | 113 + .../shaders/tonemapping_utils.slang | 34 + attachments/openxr_engine/xr_context.cpp | 694 +++ attachments/openxr_engine/xr_context.h | 127 + .../01_introduction.adoc | 26 + .../02_system_integration.adoc | 81 + .../03_hardware_alignment_luid.adoc | 63 + .../04_vulkan_1_4_feature_requirements.adoc | 72 + .../05_incorporating_into_the_engine.adoc | 179 + .../01_introduction.adoc | 26 + .../02_external_image_negotiation.adoc | 74 + .../03_raii_resource_integration.adoc | 75 + .../04_memory_ownership_lifecycle.adoc | 87 + .../05_incorporating_into_the_engine.adoc | 73 + .../04_Dynamic_Rendering/01_introduction.adoc | 16 + .../02_rendering_to_spatial_swapchains.adoc | 62 + .../03_stereo_viewport_scissor.adoc | 57 + .../04_incorporating_into_the_engine.adoc | 110 + .../01_introduction.adoc | 26 + .../02_xr_lifecycle.adoc | 71 + .../03_display_time_prediction.adoc | 56 + .../04_incorporating_into_the_engine.adoc | 112 + .../06_Late_Latching/01_introduction.adoc | 18 + .../02_last_second_update.adoc | 52 + .../06_Late_Latching/03_implementation.adoc | 63 + .../04_incorporating_into_the_engine.adoc | 60 + .../01_introduction.adoc | 17 + .../02_openxr_action_system.adoc | 80 + .../03_space_manifolds.adoc | 55 + .../04_incorporating_into_the_engine.adoc | 46 + .../01_introduction.adoc | 23 + .../02_native_multiview.adoc | 66 + .../03_slang_architecture.adoc | 61 + .../04_incorporating_into_the_engine.adoc | 76 + .../01_introduction.adoc | 14 + .../02_primary_stereo_with_insets.adoc | 60 + .../03_multi_layer_composition.adoc | 49 + .../04_incorporating_into_the_engine.adoc | 67 + .../01_introduction.adoc | 16 + .../02_fragment_density_control.adoc | 44 + .../03_gaze_driven_logic.adoc | 43 + .../04_incorporating_into_the_engine.adoc | 82 + .../11_Canted_Displays/01_introduction.adoc | 15 + .../02_non_parallel_projections.adoc | 52 + .../03_viewport_swizzling.adoc | 40 + .../04_incorporating_into_the_engine.adoc | 62 + .../12_CAVE_Architecture/01_introduction.adoc | 28 + .../02_projector_based_spatial_tech.adoc | 47 + .../03_hardware_sync.adoc | 52 + .../04_incorporating_into_the_engine.adoc | 50 + .../13_Warp_and_Blend/01_introduction.adoc | 28 + .../02_geometric_correction.adoc | 61 + .../03_post_process_warping.adoc | 56 + .../04_incorporating_into_the_engine.adoc | 74 + .../14_LightField_Theory/01_introduction.adoc | 12 + .../02_4d_lightfield_representation.adoc | 65 + .../03_high_density_view_arrays.adoc | 61 + .../04_incorporating_into_the_engine.adoc | 61 + .../01_introduction.adoc | 12 + .../02_synthesis_shaders.adoc | 50 + .../03_ray_traced_synthesis.adoc | 49 + .../04_incorporating_into_the_engine.adoc | 75 + .../01_introduction.adoc | 10 + .../02_environmental_ingestion.adoc | 43 + .../03_zero_copy_hand_off.adoc | 45 + .../04_incorporating_into_the_engine.adoc | 82 + .../01_introduction.adoc | 10 + .../02_on_gpu_inference.adoc | 54 + .../03_refining_spatial_data.adoc | 51 + .../04_incorporating_into_the_engine.adoc | 76 + .../01_introduction.adoc | 10 + .../02_ml_driven_segmentation.adoc | 51 + .../03_per_pixel_masking.adoc | 48 + .../04_incorporating_into_the_engine.adoc | 60 + .../01_introduction.adoc | 10 + .../02_desktop_high_end.adoc | 40 + .../03_mobile_mastery.adoc | 32 + .../04_incorporating_into_the_engine.adoc | 64 + .../01_introduction.adoc | 12 + .../02_spatial_debugging.adoc | 39 + .../03_automated_qa.adoc | 36 + .../04_incorporating_into_the_engine.adoc | 63 + .../conclusion.adoc | 10 + .../introduction.adoc | 57 + 110 files changed, 22428 insertions(+) create mode 100644 .github/workflows/openxr_engine_ci.yml create mode 100644 attachments/openxr_engine/CMakeLists.txt create mode 100644 attachments/openxr_engine/README.adoc create mode 100644 attachments/openxr_engine/camera_component.cpp create mode 100644 attachments/openxr_engine/camera_component.h create mode 100644 attachments/openxr_engine/engine.cpp create mode 100644 attachments/openxr_engine/engine.h create mode 100755 attachments/openxr_engine/install_dependencies_linux.sh create mode 100644 attachments/openxr_engine/install_dependencies_windows.bat create mode 100644 attachments/openxr_engine/main.cpp create mode 100644 attachments/openxr_engine/renderer.h create mode 100644 attachments/openxr_engine/renderer_core.cpp create mode 100644 attachments/openxr_engine/renderer_pipelines.cpp create mode 100644 attachments/openxr_engine/renderer_rendering.cpp create mode 100644 attachments/openxr_engine/renderer_resources.cpp create mode 100644 attachments/openxr_engine/shaders/common_types.slang create mode 100644 attachments/openxr_engine/shaders/composite.slang create mode 100644 attachments/openxr_engine/shaders/forward_plus_cull.slang create mode 100644 attachments/openxr_engine/shaders/hrtf.slang create mode 100644 attachments/openxr_engine/shaders/imgui.slang create mode 100644 attachments/openxr_engine/shaders/lighting.slang create mode 100644 attachments/openxr_engine/shaders/lighting_utils.slang create mode 100644 attachments/openxr_engine/shaders/pbr.slang create mode 100644 attachments/openxr_engine/shaders/pbr_utils.slang create mode 100644 attachments/openxr_engine/shaders/physics.slang create mode 100644 attachments/openxr_engine/shaders/ray_query.slang create mode 100644 attachments/openxr_engine/shaders/texturedMesh.slang create mode 100644 attachments/openxr_engine/shaders/tonemapping_utils.slang create mode 100644 attachments/openxr_engine/xr_context.cpp create mode 100644 attachments/openxr_engine/xr_context.h create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc create mode 100644 en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc diff --git a/.github/workflows/openxr_engine_ci.yml b/.github/workflows/openxr_engine_ci.yml new file mode 100644 index 000000000..901130236 --- /dev/null +++ b/.github/workflows/openxr_engine_ci.yml @@ -0,0 +1,161 @@ +name: OpenXR Engine CI + +on: + push: + paths: + - 'attachments/openxr_engine/**' + - 'attachments/simple_engine/**' + - '.github/workflows/openxr_engine_ci.yml' + pull_request: + paths: + - 'attachments/openxr_engine/**' + - 'attachments/simple_engine/**' + - '.github/workflows/openxr_engine_ci.yml' + workflow_dispatch: + +jobs: + desktop: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + defaults: + run: + working-directory: attachments/openxr_engine + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Clang + Ninja + ccache (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y clang ninja-build ccache libopenxr-dev + + - name: Select Clang toolchain (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + + - name: Set up MSVC dev environment + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set up Ninja + sccache + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install -y ninja sccache + $chocoBin = "C:\ProgramData\chocolatey\bin" + if (Test-Path $chocoBin) { + $chocoBin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + } + "SCCACHE_DIR=$env:LOCALAPPDATA\Mozilla\sccache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: ccache (Linux) + if: runner.os == 'Linux' + uses: actions/cache@v4 + with: + path: ~/.cache/ccache + key: ${{ runner.os }}-openxr-ccache-${{ github.sha }} + restore-keys: ${{ runner.os }}-openxr-ccache- + + - name: sccache (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v4 + with: + path: ${{ env.SCCACHE_DIR }} + key: ${{ runner.os }}-openxr-sccache-${{ github.sha }} + restore-keys: ${{ runner.os }}-openxr-sccache- + + - name: Cache Vulkan SDK (Windows) + if: runner.os == 'Windows' + id: cache-vulkan-windows + uses: actions/cache@v4 + with: + path: C:\VulkanSDK + key: ${{ runner.os }}-vulkan-sdk + + - name: Install Vulkan SDK (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + if ("${{ steps.cache-vulkan-windows.outputs.cache-hit }}" -ne "true") { + choco install -y aria2 + $installer = Join-Path $env:TEMP "vulkan-sdk.exe" + aria2c --split=8 --max-connection-per-server=8 --min-split-size=1M --dir="$env:TEMP" --out="vulkan-sdk.exe" "https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe" + Start-Process -FilePath $installer -ArgumentList "--accept-licenses --default-answer --confirm-command install" -Wait -NoNewWindow + } + $vulkanPath = Get-ChildItem "C:\VulkanSDK" | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty FullName + "VULKAN_SDK=$vulkanPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "$vulkanPath\Bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + "CMAKE_PREFIX_PATH=$vulkanPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install Vulkan SDK (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y curl xz-utils + VULKAN_VERSION=$(curl -s https://vulkan.lunarg.com/sdk/latest/linux.txt) + SDK_TGZ="${RUNNER_TEMP}/vulkansdk-linux.tar.xz" + curl -L -o "$SDK_TGZ" "https://sdk.lunarg.com/sdk/download/$VULKAN_VERSION/linux/vulkansdk-linux-x86_64-$VULKAN_VERSION.tar.xz" + mkdir -p "${RUNNER_TEMP}/VulkanSDK" + tar -xJf "$SDK_TGZ" -C "${RUNNER_TEMP}/VulkanSDK" + VULKAN_SDK_PATH=$(find "${RUNNER_TEMP}/VulkanSDK" -maxdepth 1 -type d -name '1.*' | head -n 1) + SDK_SYSROOT="$VULKAN_SDK_PATH/x86_64" + echo "VULKAN_SDK=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" + echo "VULKAN_SDK_SYSROOT=$SDK_SYSROOT" >> "$GITHUB_ENV" + echo "$SDK_SYSROOT/bin" >> "$GITHUB_PATH" + echo "CMAKE_PREFIX_PATH=$SDK_SYSROOT" >> "$GITHUB_ENV" + echo "Vulkan_INCLUDE_DIR=$SDK_SYSROOT/include" >> "$GITHUB_ENV" + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $vcpkgRoot = Join-Path $env:RUNNER_TEMP "vcpkg" + if (-not (Test-Path $vcpkgRoot)) { + git clone https://github.com/microsoft/vcpkg $vcpkgRoot + } + Push-Location $vcpkgRoot + .\bootstrap-vcpkg.bat + Pop-Location + "VCPKG_INSTALLATION_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "CMAKE_TOOLCHAIN_FILE=$vcpkgRoot\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + shell: cmd + run: | + call install_dependencies_windows.bat + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + bash install_dependencies_linux.sh + + - name: Configure (Windows) + if: runner.os == 'Windows' + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:CMAKE_TOOLCHAIN_FILE" -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DCMAKE_C_COMPILER_LAUNCHER=sccache + + - name: Configure (Linux) + if: runner.os == 'Linux' + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_PREFIX_PATH="/home/runner/.local;${{ env.VULKAN_SDK_SYSROOT }}" + + - name: Build + run: cmake --build build --target OpenXREngine --parallel 4 diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt new file mode 100644 index 000000000..13a3b4c66 --- /dev/null +++ b/attachments/openxr_engine/CMakeLists.txt @@ -0,0 +1,253 @@ +cmake_minimum_required(VERSION 3.29) + +project(OpenXREngine VERSION 1.0.0 LANGUAGES CXX C) + +# Option to enable/disable Vulkan C++20 module support for this standalone project +option(ENABLE_CPP20_MODULE "Enable C++ 20 module support for Vulkan in OpenXREngine" OFF) + +# Enable C++ module dependency scanning only when modules are enabled +if(ENABLE_CPP20_MODULE) + set(CMAKE_CXX_SCAN_FOR_MODULES ON) +endif() + +# Path to the base Simple Engine +set(SIMPLE_ENGINE_DIR "${CMAKE_CURRENT_LIST_DIR}/../simple_engine") + +# Add CMake module path for custom find modules +list(APPEND CMAKE_MODULE_PATH "${SIMPLE_ENGINE_DIR}/CMake") + +# Find required packages +find_package (glm REQUIRED) +find_package (Vulkan REQUIRED) +find_package (tinygltf REQUIRED) +find_package (KTX REQUIRED) + +# Find or download Vulkan-Hpp headers matching the Vulkan SDK/NDK version +find_package(VulkanHpp REQUIRED) + +# OpenXR is REQUIRED for this engine variant +find_package(OpenXR REQUIRED) + +if(ENABLE_CPP20_MODULE) + # Set up Vulkan C++ module + add_library(VulkanCppModule) + add_library(Vulkan::cppm ALIAS VulkanCppModule) + + target_compile_definitions(VulkanCppModule + PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 + ) + target_include_directories(VulkanCppModule + PUBLIC + "${Vulkan_INCLUDE_DIR}" + "${VulkanHpp_INCLUDE_DIRS}" + ) + target_link_libraries(VulkanCppModule + PUBLIC + Vulkan::Vulkan + ) + + set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + + target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${VulkanHpp_CPPM_DIR}" + FILES + "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" + ) + + # MSVC-specific options to improve module support + if(MSVC) + target_compile_options(VulkanCppModule PRIVATE + /std:c++latest + /permissive- + /Zc:__cplusplus + /EHsc + /Zc:preprocessor + ) + endif() +else() + add_library(VulkanCppModule INTERFACE) + add_library(Vulkan::cppm ALIAS VulkanCppModule) + target_link_libraries(VulkanCppModule INTERFACE Vulkan::Vulkan) + target_compile_definitions(VulkanCppModule + INTERFACE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 + ) + target_include_directories(VulkanCppModule INTERFACE "${VulkanHpp_INCLUDE_DIRS}") +endif() + +# Platform-specific settings +if(ANDROID) + # Android-specific settings + add_definitions(-DPLATFORM_ANDROID) + find_package(game-activity REQUIRED CONFIG) +else() + # Desktop-specific settings + add_definitions(-DPLATFORM_DESKTOP) + find_package(glfw3 REQUIRED) + find_package(OpenAL REQUIRED) +endif() + +# Shader compilation (using local shaders in openxr_engine) +file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_LIST_DIR}/shaders/*.slang) +list(FILTER SLANG_SHADER_SOURCES EXCLUDE REGEX ".*/(common_types|pbr_utils|lighting_utils|tonemapping_utils)\\.slang$") + +# Find slangc executable +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin) + +if(SLANGC_EXECUTABLE) + # Ensure the output directory for compiled shaders exists + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders) + + # Compile Slang shaders using slangc + foreach(SHADER ${SLANG_SHADER_SOURCES}) + get_filename_component(SHADER_NAME ${SHADER} NAME) + string(REGEX REPLACE "\.slang$" "" OUTPUT_NAME ${SHADER_NAME}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER} -target spirv -profile spirv_1_4 -emit-spirv-directly -o ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + DEPENDS ${SHADER} + COMMENT "Compiling Slang shader ${SHADER_NAME} with slangc" + ) + list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) + endforeach() + + add_custom_target(shaders DEPENDS ${SHADER_SPVS}) +else() + add_custom_target(shaders) +endif() + +# Source files +# We reference most files from Simple Engine via SIMPLE_ENGINE_DIR. +# Local files are those modified for OpenXR support. +set(SOURCES_COMMON + engine.cpp # OpenXR version + renderer_core.cpp # OpenXR version + renderer_rendering.cpp # OpenXR version + renderer_resources.cpp # OpenXR version + camera_component.cpp # OpenXR version + xr_context.cpp # OpenXR only + + ${SIMPLE_ENGINE_DIR}/scene_loading.cpp + ${SIMPLE_ENGINE_DIR}/platform.cpp + renderer_pipelines.cpp + ${SIMPLE_ENGINE_DIR}/renderer_compute.cpp + ${SIMPLE_ENGINE_DIR}/renderer_utils.cpp + ${SIMPLE_ENGINE_DIR}/renderer_ray_query.cpp + ${SIMPLE_ENGINE_DIR}/memory_pool.cpp + ${SIMPLE_ENGINE_DIR}/resource_manager.cpp + ${SIMPLE_ENGINE_DIR}/entity.cpp + ${SIMPLE_ENGINE_DIR}/component.cpp + ${SIMPLE_ENGINE_DIR}/transform_component.cpp + ${SIMPLE_ENGINE_DIR}/mesh_component.cpp + ${SIMPLE_ENGINE_DIR}/animation_component.cpp + ${SIMPLE_ENGINE_DIR}/model_loader.cpp + ${SIMPLE_ENGINE_DIR}/audio_system.cpp + ${SIMPLE_ENGINE_DIR}/physics_system.cpp + ${SIMPLE_ENGINE_DIR}/imgui_system.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui_draw.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui_tables.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui_widgets.cpp + ${SIMPLE_ENGINE_DIR}/vulkan_device.cpp + ${SIMPLE_ENGINE_DIR}/pipeline.cpp + ${SIMPLE_ENGINE_DIR}/descriptor_manager.cpp + ${SIMPLE_ENGINE_DIR}/renderdoc_debug_system.cpp + ${SIMPLE_ENGINE_DIR}/mikktspace.c +) + +set(SOURCES_DESKTOP + main.cpp +) + +# Create target +if (ANDROID) + add_library(OpenXREngine STATIC ${SOURCES_COMMON}) +else () + add_executable(OpenXREngine ${SOURCES_COMMON} ${SOURCES_DESKTOP}) +endif () + +add_dependencies(OpenXREngine shaders) +set_target_properties (OpenXREngine PROPERTIES CXX_STANDARD 20) + +# Include directories +target_include_directories(OpenXREngine PRIVATE + . + ${SIMPLE_ENGINE_DIR} + ${SIMPLE_ENGINE_DIR}/imgui +) + +# Enable required defines +target_compile_definitions(OpenXREngine PRIVATE + GLM_ENABLE_EXPERIMENTAL + _USE_MATH_DEFINES + VULKAN_HPP_NO_STRUCT_CONSTRUCTORS + VULKAN_HPP_DISPATCH_LOADER_DYNAMIC +) + +# Link libraries +target_link_libraries(OpenXREngine PUBLIC + Vulkan::cppm + glm::glm + tinygltf::tinygltf + KTX::ktx + OpenXR::OpenXR +) + +if (ANDROID) + target_link_libraries(OpenXREngine PUBLIC game-activity::game-activity OpenSLES android log) +else () + target_link_libraries(OpenXREngine PRIVATE glfw OpenAL::OpenAL) +endif() + +# Windows/MSVC portability and build settings +if(MSVC) + target_compile_definitions(OpenXREngine PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) + target_compile_options(OpenXREngine PRIVATE + /permissive- + /Zc:__cplusplus + /EHsc + /W3 + /MP + /bigobj + ) + target_link_libraries(OpenXREngine PRIVATE Dbghelp) +elseif(WIN32) + target_compile_definitions(OpenXREngine PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) +endif() + +# Copy Assets from Simple Engine +if(EXISTS ${SIMPLE_ENGINE_DIR}/Assets) + if (NOT ANDROID) + add_custom_command(TARGET OpenXREngine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${SIMPLE_ENGINE_DIR}/Assets ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Assets + COMMENT "Copying Assets from Simple Engine to output directory" + ) +endif() +endif () + +# Packaging configuration +include(CPack) +set(CPACK_PACKAGE_NAME "OpenXREngine") +set(CPACK_PACKAGE_VENDOR "OpenXREngine Team") +set(CPACK_PACKAGE_VERSION "1.0.0") + +if (NOT ANDROID) + install(TARGETS OpenXREngine DESTINATION bin) + if(SLANGC_EXECUTABLE) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders DESTINATION share/OpenXREngine) + endif() + # Assets are copied from Simple Engine + if(EXISTS ${SIMPLE_ENGINE_DIR}/Assets) + install(DIRECTORY ${SIMPLE_ENGINE_DIR}/Assets DESTINATION share/OpenXREngine) + endif() +endif () diff --git a/attachments/openxr_engine/README.adoc b/attachments/openxr_engine/README.adoc new file mode 100644 index 000000000..6eec9ef5f --- /dev/null +++ b/attachments/openxr_engine/README.adoc @@ -0,0 +1,13 @@ += OpenXR Engine + +This directory contains a variant of the Simple Engine that incorporates OpenXR support for Vulkan. + +== Dependency + +This project depends on the `simple_engine` located in the sibling directory `../simple_engine`. +The `CMakeLists.txt` is configured to pull shared source files, headers, and assets directly from the `simple_engine` directory to avoid redundancy. + +== Building + +To build this project, ensure that you have the Vulkan SDK and OpenXR SDK installed. +Since it references files from `simple_engine`, do not move this directory relative to the `simple_engine` directory. diff --git a/attachments/openxr_engine/camera_component.cpp b/attachments/openxr_engine/camera_component.cpp new file mode 100644 index 000000000..1e6ecc84a --- /dev/null +++ b/attachments/openxr_engine/camera_component.cpp @@ -0,0 +1,142 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "camera_component.h" +#include "xr_context.h" + +#include "entity.h" +#include + +// Most of the CameraComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Camera_Transformations chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + +// Initializes the camera by updating the view and projection matrices +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#camera-initialization +void CameraComponent::Initialize() +{ + UpdateViewMatrix(); + UpdateProjectionMatrix(); +} + +// Returns the view matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4 &CameraComponent::GetViewMatrix() +{ + if (viewMatrixDirty) + { + UpdateViewMatrix(); + } + return viewMatrix; +} + +// Returns the projection matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4 &CameraComponent::GetProjectionMatrix() +{ + if (projectionMatrixDirty) + { + UpdateProjectionMatrix(); + } + return projectionMatrix; +} + +// Updates the view matrix based on the camera's position and orientation +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#view-matrix +void CameraComponent::UpdateViewMatrix() +{ + auto transformComponent = owner->GetComponent(); + if (transformComponent) + { + // Build camera world transform (T * R) from the camera entity's transform + // and compute the view matrix as its inverse. This ensures consistency + // with rasterization and avoids relying on an external target vector. + const glm::vec3 position = transformComponent->GetPosition(); + const glm::vec3 euler = transformComponent->GetRotation(); // radians + + const glm::quat qx = glm::angleAxis(euler.x, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat qy = glm::angleAxis(euler.y, glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::quat qz = glm::angleAxis(euler.z, glm::vec3(0.0f, 0.0f, 1.0f)); + const glm::quat q = qz * qy * qx; // match TransformComponent's ZYX composition + + const glm::mat4 T = glm::translate(glm::mat4(1.0f), position); + const glm::mat4 R = glm::mat4_cast(q); + const glm::mat4 worldNoScale = T * R; + + viewMatrix = glm::inverse(worldNoScale); + } + else + { + // Fallback: default camera at origin looking towards +Z with Y up + // Note: keep consistent with right-handed convention used elsewhere + const glm::vec3 position(0.0f); + const glm::vec3 forward(0.0f, 0.0f, 1.0f); + const glm::vec3 upVec(0.0f, 1.0f, 0.0f); + viewMatrix = glm::lookAt(position, position + forward, upVec); + } + viewMatrixDirty = false; +} + +// Updates the projection matrix based on the camera's projection type and parameters +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#projection-matrix +void CameraComponent::UpdateProjectionMatrix() +{ + if (projectionType == ProjectionType::Perspective) + { + projectionMatrix = glm::perspective(glm::radians(fieldOfView), aspectRatio, nearPlane, farPlane); + } + else + { + float halfWidth = orthoWidth * 0.5f; + float halfHeight = orthoHeight * 0.5f; + projectionMatrix = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, nearPlane, farPlane); + } + projectionMatrixDirty = false; +} + +// Helper: Build asymmetric projection from FOV tangents (Chapter 5) +inline glm::mat4 getAsymmetricProjection(const XrFovf& fov, float nearZ, float farZ) { + float tanLeft = std::tan(fov.angleLeft); + float tanRight = std::tan(fov.angleRight); + float tanUp = std::tan(fov.angleUp); + float tanDown = std::tan(fov.angleDown); + + float tanWidth = tanRight - tanLeft; + float tanHeight = tanUp - tanDown; + + glm::mat4 projection(0.0f); + projection[0][0] = 2.0f / tanWidth; + projection[1][1] = 2.0f / tanHeight; + projection[2][0] = (tanRight + tanLeft) / tanWidth; + projection[2][1] = (tanUp + tanDown) / tanHeight; + projection[2][2] = -farZ / (farZ - nearZ); + projection[2][3] = -1.0f; + projection[3][2] = -(farZ * nearZ) / (farZ - nearZ); + + return projection; +} + +void CameraComponent::SetStereoViews(const XrView& left, const XrView& right) { + // 1. Convert OpenXR poses to 4x4 matrices + eyeViewMatrices[0] = xrPoseToMatrix(left.pose); + eyeViewMatrices[1] = xrPoseToMatrix(right.pose); + + // 2. Build asymmetric projection matrices from FOV tangents + eyeProjectionMatrices[0] = getAsymmetricProjection(left.fov, nearPlane, farPlane); + eyeProjectionMatrices[1] = getAsymmetricProjection(right.fov, nearPlane, farPlane); +} diff --git a/attachments/openxr_engine/camera_component.h b/attachments/openxr_engine/camera_component.h new file mode 100644 index 000000000..b2675ce5d --- /dev/null +++ b/attachments/openxr_engine/camera_component.h @@ -0,0 +1,290 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +#include "component.h" +#include "entity.h" +#include "transform_component.h" + +/** + * @brief Component that handles the camera view and projection. + * + * This class implements the camera system as described in the Camera_Transformations chapter: + * @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + */ +class CameraComponent : public Component +{ + public: + enum class ProjectionType + { + Perspective, + Orthographic + }; + + private: + ProjectionType projectionType = ProjectionType::Perspective; + + // Perspective projection parameters + float fieldOfView = 45.0f; + float aspectRatio = 16.0f / 9.0f; + + // Orthographic projection parameters + float orthoWidth = 10.0f; + float orthoHeight = 10.0f; + + // Common parameters + float nearPlane = 0.1f; + float farPlane = 100.0f; + + // Matrices + glm::mat4 viewMatrix = glm::mat4(1.0f); + glm::mat4 projectionMatrix = glm::mat4(1.0f); + + // Stereo Matrices (Chapter 5) + std::array eyeViewMatrices = {glm::mat4(1.0f), glm::mat4(1.0f)}; + std::array eyeProjectionMatrices = {glm::mat4(1.0f), glm::mat4(1.0f)}; + + // Camera properties + glm::vec3 target = {0.0f, 0.0f, 0.0f}; + glm::vec3 up = {0.0f, 1.0f, 0.0f}; + + bool viewMatrixDirty = true; + bool projectionMatrixDirty = true; + + public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit CameraComponent(const std::string &componentName = "CameraComponent") : + Component(componentName) + {} + + /** + * @brief Initialize the camera component. + */ + void Initialize() override; + + /** + * @brief Set the projection type. + * @param type The projection type. + */ + void SetProjectionType(ProjectionType type) + { + projectionType = type; + projectionMatrixDirty = true; + } + + /** + * @brief Get the projection type. + * @return The projection type. + */ + ProjectionType GetProjectionType() const + { + return projectionType; + } + + /** + * @brief Set the field of view for perspective projection. + * @param fov The field of view in degrees. + */ + void SetFieldOfView(float fov) + { + fieldOfView = fov; + projectionMatrixDirty = true; + } + + /** + * @brief Get the field of view. + * @return The field of view in degrees. + */ + float GetFieldOfView() const + { + return fieldOfView; + } + + /** + * @brief Set the aspect ratio for perspective projection. + * @param ratio The aspect ratio (width / height). + */ + void SetAspectRatio(float ratio) + { + aspectRatio = ratio; + projectionMatrixDirty = true; + } + + /** + * @brief Get the aspect ratio. + * @return The aspect ratio. + */ + float GetAspectRatio() const + { + return aspectRatio; + } + + /** + * @brief Set the orthographic width and height. + * @param width The width of the orthographic view. + * @param height The height of the orthographic view. + */ + void SetOrthographicSize(float width, float height) + { + orthoWidth = width; + orthoHeight = height; + projectionMatrixDirty = true; + } + + /** + * @brief Set the near and far planes. + * @param near The near plane distance. + * @param far The far plane distance. + */ + void SetClipPlanes(float near, float far) + { + nearPlane = near; + farPlane = far; + projectionMatrixDirty = true; + } + + float GetNearPlane() const + { + return nearPlane; + } + float GetFarPlane() const + { + return farPlane; + } + + /** + * @brief Set the camera target. + * @param newTarget The new target position. + */ + void SetTarget(const glm::vec3 &newTarget) + { + target = newTarget; + viewMatrixDirty = true; + } + + /** + * @brief Set the camera up vector. + * @param newUp The new up vector. + */ + void SetUp(const glm::vec3 &newUp) + { + up = newUp; + viewMatrixDirty = true; + } + + /** + * @brief Make the camera look at a specific target position. + * @param targetPosition The position to look at. + * @param upVector The up vector (optional, defaults to current up vector). + */ + void LookAt(const glm::vec3 &targetPosition, const glm::vec3 &upVector = glm::vec3(0.0f, 1.0f, 0.0f)) + { + target = targetPosition; + up = upVector; + viewMatrixDirty = true; + } + + /** + * @brief Get the view matrix. + * @return The view matrix. + */ + const glm::mat4 &GetViewMatrix(); + + /** + * @brief Get the projection matrix. + * @return The projection matrix. + */ + const glm::mat4 &GetProjectionMatrix(); + + /** + * @brief Get the camera position. + * @return The camera position. + */ + glm::vec3 GetPosition() const + { + auto transform = GetOwner()->GetComponent(); + return transform ? transform->GetPosition() : glm::vec3(0.0f, 0.0f, 0.0f); + } + + /** + * @brief Get the camera target. + * @return The camera target. + */ + const glm::vec3 &GetTarget() const + { + return target; + } + + /** + * @brief Get the camera up vector. + * @return The camera up vector. + */ + const glm::vec3 &GetUp() const + { + return up; + } + + /** + * @brief Force view matrix recalculation without modifying camera orientation. + * This is used when the camera's transform position changes externally (e.g., from GLTF loading). + */ + void ForceViewMatrixUpdate() + { + viewMatrixDirty = true; + } + + /** + * @brief Set the stereo views for XR (Chapter 5). + * @param left The left eye view data. + * @param right The right eye view data. + */ + void SetStereoViews(const XrView& left, const XrView& right); + + /** + * @brief Get the eye-specific view matrix. + * @param eye The eye index (0 for left, 1 for right). + * @return The view matrix. + */ + const glm::mat4& GetViewMatrix(uint32_t eye) const { return eyeViewMatrices[eye]; } + + /** + * @brief Get the eye-specific projection matrix. + * @param eye The eye index (0 for left, 1 for right). + * @return The projection matrix. + */ + const glm::mat4& GetProjectionMatrix(uint32_t eye) const { return eyeProjectionMatrices[eye]; } + + private: + /** + * @brief Update the view matrix based on the camera position and target. + */ + void UpdateViewMatrix(); + + /** + * @brief Update the projection matrix based on the projection type and parameters. + */ + void UpdateProjectionMatrix(); +}; diff --git a/attachments/openxr_engine/engine.cpp b/attachments/openxr_engine/engine.cpp new file mode 100644 index 000000000..6c5303e6c --- /dev/null +++ b/attachments/openxr_engine/engine.cpp @@ -0,0 +1,1098 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "engine.h" +#include "mesh_component.h" +#include "scene_loading.h" + +#include +#include +#include +#include +#include +#include + +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + +Engine::Engine() : resourceManager(std::make_unique()) { +} + +bool Engine::IsMainThread() const { + return std::this_thread::get_id() == mainThreadId; +} + +void Engine::ProcessPendingEntityRemovals() { + std::vector names; { + std::lock_guard lk(pendingEntityRemovalsMutex); + if (pendingEntityRemovalNames.empty()) + return; + names.swap(pendingEntityRemovalNames); + } + + // Process on the main thread only (safety) + if (!IsMainThread()) { + // Put them back; we'll retry next main-thread tick + std::lock_guard lk(pendingEntityRemovalsMutex); + pendingEntityRemovalNames.insert(pendingEntityRemovalNames.end(), names.begin(), names.end()); + return; + } + + // Apply removals using the normal API (which takes the appropriate locks). + for (const auto& name : names) { + (void) RemoveEntity(name); + } +} + +Engine::~Engine() { + Cleanup(); +} + +bool Engine::Initialize(const std::string& appName, int width, int height, bool enableValidationLayers, bool useXR) { + // Create platform +#if defined(PLATFORM_ANDROID) + // For Android, the platform is created with the android_app + // This will be handled in the android_main function + return false; +#else + // Record main thread identity for deferring destructive operations from background threads + mainThreadId = std::this_thread::get_id(); + + platform = CreatePlatform(); + if (!platform->Initialize(appName, width, height)) { + return false; + } + + // Set resize callback + platform->SetResizeCallback([this](int width, int height) { + HandleResize(width, height); + }); + + // Set mouse callback + platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + handleMouseInput(x, y, buttons); + }); + + // Set keyboard callback + platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { + handleKeyInput(key, pressed); + }); + + // Set char callback + platform->SetCharCallback([this](uint32_t c) { + if (imguiSystem) { + imguiSystem->HandleChar(c); + } + }); + + // Create renderer + renderer = std::make_unique(platform.get()); + if (!renderer->Initialize(appName, enableValidationLayers, useXR)) { + return false; + } + + try { + // Model loader via constructor; also wire into renderer + modelLoader = std::make_unique(renderer.get()); + renderer->SetModelLoader(modelLoader.get()); + + // Audio system via constructor + audioSystem = std::make_unique(this, renderer.get()); + + // Physics system via constructor (GPU enabled) + physicsSystem = std::make_unique(renderer.get(), true); + + // ImGui via constructor, then connect audio system + imguiSystem = std::make_unique(renderer.get(), width, height); + imguiSystem->SetAudioSystem(audioSystem.get()); + } catch (const std::exception& e) { + std::cerr << "Subsystem initialization failed: " << e.what() << std::endl; + return false; + } + + // Generate ball material properties once at load time + GenerateBallMaterial(); + + // Initialize physics scaling system + InitializePhysicsScaling(); + + initialized = true; + return true; +#endif +} + +void Engine::Run() { + if (!initialized) { + throw std::runtime_error("Engine not initialized"); + } + + running = true; + + // Main loop + while (running) { + // Process platform events + if (!platform->ProcessEvents()) { + running = false; + break; + } + + if (renderer->IsXrMode()) { + auto& xrContext = renderer->GetXrContext(); + auto frameState = xrContext.waitFrame(); + xrContext.beginFrame(); + + deltaTimeMs = CalculateDeltaTimeMs(); + Update(frameState.predictedDisplayTime); + Render(frameState.predictedDisplayTime); + + xrContext.endFrame(renderer->GetXrImageViews()); + + // Update frame counter and FPS for window title (companion window) + frameCount++; + fpsUpdateTimer += deltaTimeMs.count() * 0.001f; + if (fpsUpdateTimer >= 1.0f) { + UpdateFPSWindowTitle(); + } + } else { + // Calculate delta time + deltaTimeMs = CalculateDeltaTimeMs(); + + // Update frame counter and FPS + frameCount++; + fpsUpdateTimer += deltaTimeMs.count() * 0.001f; + + // Update window title with FPS and frame time every second + if (fpsUpdateTimer >= 1.0f) { + UpdateFPSWindowTitle(); + } + + // Update + Update(deltaTimeMs); + + // Render + Render(); + } + } +} + +void Engine::Cleanup() { + if (initialized) { + // Wait for the device to be idle before cleaning up + if (renderer) { + renderer->WaitIdle(); + } + + // Clear entities + { + std::unique_lock lk(entitiesMutex); + entities.clear(); + entityMap.clear(); + } + + // Clean up subsystems in reverse order of creation + imguiSystem.reset(); + physicsSystem.reset(); + audioSystem.reset(); + modelLoader.reset(); + renderer.reset(); + platform.reset(); + + initialized = false; + } +} + +Entity* Engine::CreateEntity(const std::string& name) { + std::unique_lock lk(entitiesMutex); + // Always allow duplicate names; map stores a representative entity + // Create the entity + auto entity = std::make_unique(name); + // Add to the vector and map + entities.push_back(std::move(entity)); + Entity* rawPtr = entities.back().get(); + // Update the map to point to the most recently created entity with this name + entityMap[name] = rawPtr; + + return rawPtr; +} + +Entity* Engine::GetEntity(const std::string& name) { + std::shared_lock lk(entitiesMutex); + auto it = entityMap.find(name); + if (it != entityMap.end()) { + return it->second; + } + return nullptr; +} + +bool Engine::RemoveEntity(Entity* entity) { + if (!entity) { + return false; + } + + // If called from a background thread, defer removal to avoid deleting entities + // while the render thread may be iterating a snapshot. + if (!IsMainThread()) { + std::lock_guard lk(pendingEntityRemovalsMutex); + pendingEntityRemovalNames.push_back(entity->GetName()); + return true; + } + + std::unique_lock lk(entitiesMutex); + + // Remember the name before erasing ownership + std::string name = entity->GetName(); + + // Find the entity in the vector + auto it = std::ranges::find_if(entities, + [entity](const std::unique_ptr& e) { + return e.get() == entity; + }); + + if (it != entities.end()) { + // Remove from the vector (ownership) + entities.erase(it); + + // Update the map: point to another entity with the same name if one exists + auto remainingIt = std::ranges::find_if(entities, + [&name](const std::unique_ptr& e) { + return e->GetName() == name; + }); + + if (remainingIt != entities.end()) { + entityMap[name] = remainingIt->get(); + } else { + entityMap.erase(name); + } + + return true; + } + + return false; +} + +bool Engine::RemoveEntity(const std::string& name) { + // If called from a background thread, defer removal to avoid deleting entities + // while the render thread may be iterating a snapshot. + if (!IsMainThread()) { + std::lock_guard lk(pendingEntityRemovalsMutex); + pendingEntityRemovalNames.push_back(name); + return true; + } + + std::unique_lock lk(entitiesMutex); + auto it = entityMap.find(name); + if (it == entityMap.end()) + return false; + Entity* entity = it->second; + if (!entity) + return false; + + // Find the entity in the vector + auto vecIt = std::ranges::find_if(entities, + [entity](const std::unique_ptr& e) { + return e.get() == entity; + }); + if (vecIt == entities.end()) { + entityMap.erase(name); + return false; + } + + entities.erase(vecIt); + + // Update the map: point to another entity with the same name if one exists + auto remainingIt = std::ranges::find_if(entities, + [&name](const std::unique_ptr& e) { + return e && e->GetName() == name; + }); + if (remainingIt != entities.end()) { + entityMap[name] = remainingIt->get(); + } else { + entityMap.erase(name); + } + return true; +} + +void Engine::SetActiveCamera(CameraComponent* cameraComponent) { + activeCamera = cameraComponent; +} + +const CameraComponent* Engine::GetActiveCamera() const { + return activeCamera; +} + +const ResourceManager* Engine::GetResourceManager() const { + return resourceManager.get(); +} + +const Platform* Engine::GetPlatform() const { + return platform.get(); +} + +Renderer* Engine::GetRenderer() { + return renderer.get(); +} + +ModelLoader* Engine::GetModelLoader() { + return modelLoader.get(); +} + +const AudioSystem* Engine::GetAudioSystem() const { + return audioSystem.get(); +} + +PhysicsSystem* Engine::GetPhysicsSystem() { + return physicsSystem.get(); +} + +const ImGuiSystem* Engine::GetImGuiSystem() const { + return imguiSystem.get(); +} + +void Engine::handleMouseInput(float x, float y, uint32_t buttons) { + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + // Suppress right-click while loading + if (renderer&& renderer + + -> + IsLoading() + ) { + buttons &= ~2u; // clear right button bit + } + + if (!imguiWantsMouse) { + // Handle mouse click for ball throwing (right mouse button) + if (buttons & 2) { + // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Throw a ball on mouse click + ThrowBall(x, y); + } + } else { + cameraControl.mouseRightPressed = false; + } + + // Handle camera rotation when left mouse button is pressed + if (buttons & 1) { + // Left mouse button (bit 0) + if (!cameraControl.mouseLeftPressed) { + cameraControl.mouseLeftPressed = true; + cameraControl.firstMouse = true; + } + + if (cameraControl.firstMouse) { + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + cameraControl.firstMouse = false; + } + + float xOffset = x - cameraControl.lastMouseX; + float yOffset = y - cameraControl.lastMouseY; + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + + xOffset *= cameraControl.mouseSensitivity; + yOffset *= cameraControl.mouseSensitivity; + + // Mouse look: positive X moves view to the right; positive Y moves view up. + // Platform mouse coordinates increase downward, so invert Y. + cameraControl.yaw -= xOffset; + cameraControl.pitch -= yOffset; + + // Constrain pitch to avoid gimbal lock + if (cameraControl.pitch > 89.0f) + cameraControl.pitch = 89.0f; + if (cameraControl.pitch < -89.0f) + cameraControl.pitch = -89.0f; + } else { + cameraControl.mouseLeftPressed = false; + } + } + + if (imguiSystem) { + imguiSystem->HandleMouse(x, y, buttons); + } + + // Always perform hover detection (even when ImGui is active) + HandleMouseHover(x, y); +} +void Engine::handleKeyInput(uint32_t key, bool pressed) { +#if !defined(PLATFORM_ANDROID) + switch (key) { + case GLFW_KEY_W: + case GLFW_KEY_UP: + cameraControl.moveForward = pressed; + break; + case GLFW_KEY_S: + case GLFW_KEY_DOWN: + cameraControl.moveBackward = pressed; + break; + case GLFW_KEY_A: + case GLFW_KEY_LEFT: + cameraControl.moveLeft = pressed; + break; + case GLFW_KEY_D: + case GLFW_KEY_RIGHT: + cameraControl.moveRight = pressed; + break; + case GLFW_KEY_Q: + case GLFW_KEY_PAGE_UP: + cameraControl.moveUp = pressed; + break; + case GLFW_KEY_E: + case GLFW_KEY_PAGE_DOWN: + cameraControl.moveDown = pressed; + break; + default: + break; + } + + if (imguiSystem) { + imguiSystem->HandleKeyboard(key, pressed); + } +#else + // Android uses different input handling via touch events + (void) key; + (void) pressed; +#endif +} + +void Engine::Update(XrTime predictedTime) { + if (renderer && renderer->IsXrMode()) { + auto& xrContext = renderer->GetXrContext(); + + // 1. Poll OpenXR action states (Grab, Teleport, etc.) (Chapter 7) + xrContext.pollActions(); + + // 2. Retrieve the predicted views (poses and FOVs) from OpenXR (Chapter 5) + xrContext.locateViews(predictedTime); + + // 3. Update the camera with the spatial data + if (activeCamera) { + auto views = xrContext.getLatestViews(); + if (views.size() >= 2) { + activeCamera->SetStereoViews(views[0], views[1]); + } + } + } + + // Use the standard update with the last calculated delta for simulation consistency + Update(deltaTimeMs); +} + +void Engine::Update(TimeDelta deltaTime) { + // Apply any entity removals requested by background threads. + ProcessPendingEntityRemovals(); + + // During background scene loading we avoid touching the live entity + // list from the main thread. This lets the loading thread construct + // entities/components safely while the main thread only drives the + // UI/loading overlay. + if (renderer&& renderer + + -> + IsLoading() + ) { + if (imguiSystem) { + imguiSystem->NewFrame(); + } + return; + } + + // Process pending ball creations (outside rendering loop to avoid memory pool constraints) + ProcessPendingBalls(); + + if (activeCamera) { + glm::vec3 currentCameraPosition = activeCamera->GetPosition(); + physicsSystem->SetCameraPosition(currentCameraPosition); + } + + // Use real deltaTime for physics to maintain proper timing + physicsSystem->Update(deltaTime); + + // Update audio system + audioSystem->Update(deltaTime); + + // Update ImGui system + imguiSystem->NewFrame(); + + // Update camera controls + if (activeCamera) { + UpdateCameraControls(deltaTime); + } + + // Update all entities. + // Do not hold `entitiesMutex` while calling `Entity::Update()`. + // Background threads may need the unique lock to add entities during loading, + // and holding a shared lock for a long time can starve them. + std::vector snapshot; { + std::shared_lock lk(entitiesMutex); + snapshot.reserve(entities.size()); + for (auto& uptr : entities) { + snapshot.push_back(uptr.get()); + } + } + for (Entity* entity : snapshot) { + if (!entity || !entity->IsActive()) + continue; + entity->Update(deltaTime); + } +} + +void Engine::Render(XrTime predictedTime) { + // Ensure renderer is ready + if (!renderer || !renderer->IsInitialized()) { + return; + } + + // Apply any entity removals requested by background threads before taking a snapshot. + ProcessPendingEntityRemovals(); + + // Snapshot entity pointers under a short shared lock, then release the lock + // before rendering. This prevents starving the background loader/physics threads + // that need the unique lock to create entities/components. + std::vector snapshot; { + std::shared_lock lk(entitiesMutex); + snapshot.reserve(entities.size()); + for (auto& uptr : entities) { + snapshot.push_back(uptr.get()); + } + } + + // Render the scene for XR (per-eye) + renderer->Render(snapshot, activeCamera, imguiSystem.get(), predictedTime); +} + +void Engine::Render() { + // Ensure renderer is ready + if (!renderer || !renderer->IsInitialized()) { + return; + } + + // Check if we have an active camera + if (!activeCamera) { + return; + } + + // Apply any entity removals requested by background threads before taking a snapshot. + ProcessPendingEntityRemovals(); + + // Snapshot entity pointers under a short shared lock, then release the lock + // before rendering. This prevents starving the background loader/physics threads + // that need the unique lock to create entities/components. + std::vector snapshot; { + std::shared_lock lk(entitiesMutex); + snapshot.reserve(entities.size()); + for (auto& uptr : entities) { + snapshot.push_back(uptr.get()); + } + } + + // Render the scene (ImGui will be rendered within the render pass) + renderer->Render(snapshot, activeCamera, imguiSystem.get()); +} + +void Engine::UpdateFPSWindowTitle() { + uint64_t framesSinceLastUpdate = frameCount - lastFPSUpdateFrame; + double avgMs = 0.0; + if (framesSinceLastUpdate > 0 && fpsUpdateTimer > 0.0f) { + currentFPS = static_cast(static_cast(framesSinceLastUpdate) / static_cast(fpsUpdateTimer)); + avgMs = (fpsUpdateTimer / static_cast(framesSinceLastUpdate)) * 1000.0; + } else { + // Avoid divide-by-zero; keep previous FPS and estimate avgMs from last delta + currentFPS = std::max(currentFPS, 1.0f); + avgMs = static_cast(deltaTimeMs.count()); + } + + // Update window title with frame count, FPS, and frame time + std::string title = (renderer && renderer->IsXrMode() ? "Simple Engine [XR] - Frame: " : "Simple Engine - Frame: ") + + std::to_string(frameCount) + + " | FPS: " + std::to_string(static_cast(currentFPS)) + + " | ms: " + std::to_string(static_cast(avgMs)); + platform->SetWindowTitle(title); + + // Reset timer and frame counter for next update + fpsUpdateTimer = 0.0f; + lastFPSUpdateFrame = frameCount; +} + +std::chrono::milliseconds Engine::CalculateDeltaTimeMs() { + // Get current time using a steady clock to avoid system time jumps + uint64_t currentTime = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + + // Initialize lastFrameTimeMs on first call + if (lastFrameTimeMs == 0) { + lastFrameTimeMs = currentTime; + return std::chrono::milliseconds(16); // ~16ms as a sane initial guess + } + + // Calculate delta time in milliseconds + uint64_t delta = currentTime - lastFrameTimeMs; + + // Update last frame time + lastFrameTimeMs = currentTime; + + return std::chrono::milliseconds(static_cast(delta)); +} + +void Engine::HandleResize(int width, int height) const { + if (height <= 0 || width <= 0) { + return; + } + // Update the active camera's aspect ratio + if (activeCamera) { + activeCamera->SetAspectRatio(static_cast(width) / static_cast(height)); + } + + // Notify the renderer that the framebuffer has been resized + if (renderer) { + renderer->SetFramebufferResized(); + } + + // Notify ImGui system about the resize + if (imguiSystem) { + imguiSystem->HandleResize(static_cast(width), static_cast(height)); + } +} + +void Engine::UpdateCameraControls(TimeDelta deltaTime) { + if (!activeCamera) + return; + + // Get a camera transform component + auto* cameraTransform = activeCamera->GetOwner()->GetComponent(); + if (!cameraTransform) + return; + + // Check if camera tracking is enabled + if (imguiSystem&& imguiSystem + + -> + IsCameraTrackingEnabled() + ) { + // Find the first active ball entity + Entity* ballEntity = nullptr; { + std::shared_lock lk(entitiesMutex); + auto ballEntityIt = std::ranges::find_if(entities, + [](auto const& entity) { + return entity && entity->IsActive() && (entity->GetName().find("Ball_") != std::string::npos); + }); + ballEntity = (ballEntityIt != entities.end()) ? ballEntityIt->get() : nullptr; + } + + if (ballEntity) { + // Get ball's transform component + auto* ballTransform = ballEntity->GetComponent(); + if (ballTransform) { + glm::vec3 ballPosition = ballTransform->GetPosition(); + + // Position camera at a fixed offset from the ball for good viewing + glm::vec3 cameraOffset = glm::vec3(2.0f, 1.5f, 2.0f); // Behind and above the ball + glm::vec3 cameraPosition = ballPosition + cameraOffset; + + // Update camera position and target + cameraTransform->SetPosition(cameraPosition); + activeCamera->SetTarget(ballPosition); + + return; // Skip manual controls when tracking + } + } + } + + // Manual camera controls (only when tracking is disabled) + // Calculate movement speed + float velocity = cameraControl.cameraSpeed * deltaTime.count() * .001f; + + // Capture base orientation from GLTF camera once and then apply mouse deltas relative to it + if (!cameraControl.baseOrientationCaptured) { + // TransformComponent stores Euler in radians; convert to quaternion + glm::vec3 baseEuler = cameraTransform->GetRotation(); + const glm::quat qx = glm::angleAxis(baseEuler.x, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat qy = glm::angleAxis(baseEuler.y, glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::quat qz = glm::angleAxis(baseEuler.z, glm::vec3(0.0f, 0.0f, 1.0f)); + // Match CameraComponent::UpdateViewMatrix composition (q = qz * qy * qx) + cameraControl.baseOrientation = qz * qy * qx; + cameraControl.baseOrientationCaptured = true; + } + + // Build delta orientation from yaw/pitch mouse deltas (degrees -> radians) + const float yawRad = glm::radians(cameraControl.yaw); + const float pitchRad = glm::radians(cameraControl.pitch); + const glm::quat qDeltaY = glm::angleAxis(yawRad, glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::quat qDeltaX = glm::angleAxis(pitchRad, glm::vec3(1.0f, 0.0f, 0.0f)); + // Apply yaw then pitch in the same convention as CameraComponent (ZYX overall), so delta = Ry * Rx + glm::quat qDelta = qDeltaY * qDeltaX; + glm::quat qFinal = cameraControl.baseOrientation * qDelta; + + // Derive camera basis directly from rotated axes to avoid ambiguity + glm::vec3 right = glm::normalize(qFinal * glm::vec3(1.0f, 0.0f, 0.0f)); + glm::vec3 up = glm::normalize(qFinal * glm::vec3(0.0f, 1.0f, 0.0f)); + // Camera forward in world space. + // Our view/projection conventions assume the camera looks down -Z in its local space. + glm::vec3 front = glm::normalize(qFinal * glm::vec3(0.0f, 0.0f, -1.0f)); + + // Get the current camera position + glm::vec3 position = cameraTransform->GetPosition(); + + // Apply movement based on input + if (cameraControl.moveForward) { + position += front * velocity; + } + if (cameraControl.moveBackward) { + position -= front * velocity; + } + if (cameraControl.moveLeft) { + position -= right * velocity; + } + if (cameraControl.moveRight) { + position += right * velocity; + } + if (cameraControl.moveUp) { + position += up * velocity; + } + if (cameraControl.moveDown) { + position -= up * velocity; + } + + // Update camera position + cameraTransform->SetPosition(position); + // Apply rotation to the camera transform based on GLTF base orientation plus mouse deltas + // TransformComponent expects radians Euler (ZYX order in our CameraComponent). + cameraTransform->SetRotation(glm::eulerAngles(qFinal)); + + // Update camera target based on a direction + glm::vec3 target = position + front; + activeCamera->SetTarget(target); + + // Ensure the camera view matrix reflects the new transform immediately this frame + activeCamera->ForceViewMatrixUpdate(); +} + +void Engine::GenerateBallMaterial() { + // Generate 8 random material properties for PBR + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(0.0f, 1.0f); + + // Generate bright, vibrant albedo colors for better visibility + std::uniform_real_distribution brightDis(0.6f, 1.0f); // Ensure bright colors + ballMaterial.albedo = glm::vec3(brightDis(gen), brightDis(gen), brightDis(gen)); + + // Random metallic value (0.0 to 1.0) + ballMaterial.metallic = dis(gen); + + // Random roughness value (0.0 to 1.0) + ballMaterial.roughness = dis(gen); + + // Random ambient occlusion (typically 0.8 to 1.0 for good lighting) + ballMaterial.ao = 0.8f + dis(gen) * 0.2f; + + // Random emissive color (usually subtle) + ballMaterial.emissive = glm::vec3(dis(gen) * 0.3f, dis(gen) * 0.3f, dis(gen) * 0.3f); + + // Decent bounciness (0.6 to 0.9) so bounces are clearly visible + ballMaterial.bounciness = 0.6f + dis(gen) * 0.3f; +} + +void Engine::InitializePhysicsScaling() { + // Based on issue analysis: balls reaching 120+ m/s and extreme positions like (-244, -360, -244) + // The previous 200.0f force scale was causing supersonic speeds and balls flying out of scene + // Need much more conservative scaling for realistic visual gameplay + + // Use smaller game unit scale for more controlled physics + physicsScaling.gameUnitsToMeters = 0.1f; // 1 game unit = 0.1 meter (10cm) - smaller scale + + // Much reduced force scaling to prevent extreme speeds + // With base forces 0.01f-0.05f, this gives final forces of 0.001f-0.005f + physicsScaling.forceScale = 1.0f; // Minimal force scaling for realistic movement + physicsScaling.physicsTimeScale = 1.0f; // Keep time scale normal + physicsScaling.gravityScale = 1.0f; // Keep gravity proportional to scale + + // Apply scaled gravity to physics system + glm::vec3 realWorldGravity(0.0f, -9.81f, 0.0f); + glm::vec3 scaledGravity = ScaleGravityForPhysics(realWorldGravity); + physicsSystem->SetGravity(scaledGravity); +} + +float Engine::ScaleForceForPhysics(float gameForce) const { + // Scale force based on the relationship between game units and real world + // and the force scaling factor to make physics feel right + return gameForce * physicsScaling.forceScale * physicsScaling.gameUnitsToMeters; +} + +glm::vec3 Engine::ScaleGravityForPhysics(const glm::vec3& realWorldGravity) const { + // Scale gravity based on game unit scale and gravity scaling factor + // If 1 game unit = 1 meter, then gravity should remain -9.81 + // If 1 game unit = 0.1 meter, then gravity should be -0.981 + return realWorldGravity * physicsScaling.gravityScale * physicsScaling.gameUnitsToMeters; +} + +float Engine::ScaleTimeForPhysics(float deltaTime) const { + // Scale time for physics simulation if needed + // This can be used to slow down or speed up physics relative to rendering + return deltaTime * physicsScaling.physicsTimeScale; +} + +void Engine::ThrowBall(float mouseX, float mouseY) { + if (!activeCamera || !physicsSystem) { + return; + } + + // Get window dimensions + int windowWidth, windowHeight; + platform->GetWindowSize(&windowWidth, &windowHeight); + + // Convert mouse coordinates to normalized device coordinates (-1 to 1) + float ndcX = (2.0f * mouseX) / static_cast(windowWidth) - 1.0f; + float ndcY = 1.0f - (2.0f * mouseY) / static_cast(windowHeight); + + // Get camera matrices + glm::mat4 viewMatrix = activeCamera->GetViewMatrix(); + glm::mat4 projMatrix = activeCamera->GetProjectionMatrix(); + + // Calculate inverse matrices + glm::mat4 invView = glm::inverse(viewMatrix); + glm::mat4 invProj = glm::inverse(projMatrix); + + // Convert NDC to world space for direction + glm::vec4 rayClip = glm::vec4(ndcX, ndcY, -1.0f, 1.0f); + glm::vec4 rayEye = invProj * rayClip; + rayEye = glm::vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); + glm::vec4 rayWorld = invView * rayEye; + + // Calculate screen center in world coordinates + // Screen center is at NDC (0, 0) which corresponds to the center of the view + glm::vec4 screenCenterClip = glm::vec4(0.0f, 0.0f, -1.0f, 1.0f); + glm::vec4 screenCenterEye = invProj * screenCenterClip; + screenCenterEye = glm::vec4(screenCenterEye.x, screenCenterEye.y, -1.0f, 0.0f); + glm::vec4 screenCenterWorld = invView * screenCenterEye; + glm::vec3 screenCenterDirection = glm::normalize(glm::vec3(screenCenterWorld)); + + // Calculate world position for screen center at a reasonable distance from camera + glm::vec3 cameraPosition = activeCamera->GetPosition(); + glm::vec3 screenCenterWorldPos = cameraPosition + screenCenterDirection * 2.0f; // 2 units in front of camera + + // Calculate throw direction from screen center toward mouse position + glm::vec3 throwDirection = glm::normalize(glm::vec3(rayWorld)); + + // Add upward component for realistic arc trajectory + throwDirection.y += 0.3f; // Add upward bias for throwing arc + throwDirection = glm::normalize(throwDirection); // Re-normalize after modification + + // Generate ball properties now + static int ballCounter = 0; + std::string ballName = "Ball_" + std::to_string(ballCounter++); + + std::random_device rd; + std::mt19937 gen(rd()); + + // Launch balls from screen center toward mouse cursor + glm::vec3 spawnPosition = screenCenterWorldPos; + + // Add small random variation to avoid identical paths + std::uniform_real_distribution posDis(-0.1f, 0.1f); + spawnPosition.x += posDis(gen); + spawnPosition.y += posDis(gen); + spawnPosition.z += posDis(gen); + + std::uniform_real_distribution spinDis(-10.0f, 10.0f); + std::uniform_real_distribution forceDis(15.0f, 35.0f); // Stronger force range for proper throwing feel + + // Store ball creation data for processing outside rendering loop + PendingBall pendingBall; + pendingBall.spawnPosition = spawnPosition; + pendingBall.throwDirection = throwDirection; // This is now the corrected direction toward geometry + pendingBall.throwForce = ScaleForceForPhysics(forceDis(gen)); // Apply physics scaling to force + pendingBall.randomSpin = glm::vec3(spinDis(gen), spinDis(gen), spinDis(gen)); + pendingBall.ballName = ballName; + + pendingBalls.push_back(pendingBall); +} + +void Engine::ProcessPendingBalls() { + // Process all pending balls + for (const auto& pendingBall : pendingBalls) { + // Create ball entity + Entity* ballEntity = CreateEntity(pendingBall.ballName); + if (!ballEntity) { + std::cerr << "Failed to create ball entity: " << pendingBall.ballName << std::endl; + continue; + } + + // Add transform component + auto* transform = ballEntity->AddComponent(); + if (!transform) { + std::cerr << "Failed to add TransformComponent to ball: " << pendingBall.ballName << std::endl; + continue; + } + transform->SetPosition(pendingBall.spawnPosition); + transform->SetScale(glm::vec3(1.0f)); // Tennis ball size scale + + // Add mesh component with sphere geometry + auto* mesh = ballEntity->AddComponent(); + if (!mesh) { + std::cerr << "Failed to add MeshComponent to ball: " << pendingBall.ballName << std::endl; + continue; + } + // Create tennis ball-sized, bright red sphere + glm::vec3 brightRed(1.0f, 0.0f, 0.0f); + mesh->CreateSphere(0.0335f, brightRed, 32); // Tennis ball radius, bright color, high detail + mesh->SetTexturePath(renderer->SHARED_BRIGHT_RED_ID); // Use bright red texture for visibility + + // Verify mesh geometry was created + const auto& vertices = mesh->GetVertices(); + const auto& indices = mesh->GetIndices(); + if (vertices.empty() || indices.empty()) { + std::cerr << "ERROR: CreateSphere failed to generate geometry!" << std::endl; + continue; + } + + // Pre-allocate Vulkan resources for this entity (now outside rendering loop) + if (!renderer->preAllocateEntityResources(ballEntity)) { + std::cerr << "Failed to pre-allocate resources for ball: " << pendingBall.ballName << std::endl; + continue; + } + + // Create rigid body with sphere collision shape + RigidBody* rigidBody = physicsSystem->CreateRigidBody(ballEntity, CollisionShape::Sphere, 1.0f); + if (rigidBody) { + // Set bounciness from material + rigidBody->SetRestitution(ballMaterial.bounciness); + + // Request an acceleration structure build so the new ball is included in Ray Query mode. + // We do this after creating the rigid body and initializing the entity. + renderer->RequestAccelerationStructureBuild("Ball spawned"); + + // Apply throw force and spin + glm::vec3 throwImpulse = pendingBall.throwDirection * pendingBall.throwForce; + rigidBody->ApplyImpulse(throwImpulse, glm::vec3(0.0f)); + rigidBody->SetAngularVelocity(pendingBall.randomSpin); + } + } + + // Clear processed balls + pendingBalls.clear(); +} + +void Engine::HandleMouseHover(float mouseX, float mouseY) { + // Update current mouse position for any systems that might need it + currentMouseX = mouseX; + currentMouseY = mouseY; +} + +#if defined(PLATFORM_ANDROID) +// Android-specific implementation +bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers, bool useXR) { + // Create platform + platform = CreatePlatform(app); + if (!platform->Initialize(appName, 0, 0)) { + return false; + } + + // Set resize callback + platform->SetResizeCallback([this](int width, int height) { + HandleResize(width, height); + }); + + // Set mouse callback + platform->SetMouseCallback([this](float x, float y, uint32_t buttons) { + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + if (!imguiWantsMouse) { + // Handle mouse click for ball throwing (right mouse button) + if (buttons & 2) { + // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Throw a ball on mouse click + ThrowBall(x, y); + } + } else { + cameraControl.mouseRightPressed = false; + } + } + + if (imguiSystem) { + imguiSystem->HandleMouse(x, y, buttons); + } + }); + + // Set keyboard callback + platform->SetKeyboardCallback([this](uint32_t key, bool pressed) { + if (imguiSystem) { + imguiSystem->HandleKeyboard(key, pressed); + } + }); + + // Set char callback + platform->SetCharCallback([this](uint32_t c) { + if (imguiSystem) { + imguiSystem->HandleChar(c); + } + }); + + // Create renderer + renderer = std::make_unique(platform.get()); + if (useXR) { + renderer->SetAndroidApp(app); + } + if (!renderer->Initialize(appName, enableValidationLayers, useXR)) { + return false; + } + + // Get window dimensions from platform for ImGui initialization + int width, height; + platform->GetWindowSize(&width, &height); + + try { + // Model loader via constructor; also wire into renderer + modelLoader = std::make_unique(renderer.get()); + renderer->SetModelLoader(modelLoader.get()); + + // Audio system via constructor + audioSystem = std::make_unique(this, renderer.get()); + + // Physics system via constructor (GPU enabled) + physicsSystem = std::make_unique(renderer.get(), true); + + // ImGui via constructor, then connect audio system + imguiSystem = std::make_unique(renderer.get(), width, height); + imguiSystem->SetAudioSystem(audioSystem.get()); + } catch (const std::exception& e) { + std::cerr << "Subsystem initialization failed: " << e.what() << std::endl; + return false; + } + + // Generate ball material properties once at load time + GenerateBallMaterial(); + + // Initialize physics scaling system + InitializePhysicsScaling(); + + initialized = true; + return true; +} + +void Engine::RunAndroid() { + Run(); +} +#endif diff --git a/attachments/openxr_engine/engine.h b/attachments/openxr_engine/engine.h new file mode 100644 index 000000000..65567b44f --- /dev/null +++ b/attachments/openxr_engine/engine.h @@ -0,0 +1,417 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audio_system.h" +#include "camera_component.h" +#include "entity.h" +#include "imgui_system.h" +#include "model_loader.h" +#include "physics_system.h" +#include "platform.h" +#include "renderer.h" +#include "resource_manager.h" + +/** + * @brief Main engine class that manages the game loop and subsystems. + * + * This class implements the core engine architecture as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + */ +class Engine +{ + public: + using TimeDelta = std::chrono::milliseconds; + /** + * @brief Default constructor. + */ + Engine(); + + /** + * @brief Destructor for proper cleanup. + */ + ~Engine(); + + /** + * @brief Initialize the engine. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @param enableValidationLayers Whether to enable Vulkan validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string &appName, int width, int height, bool enableValidationLayers = true, bool useXR = false); + + /** + * @brief Run the main game loop. + */ + void Run(); + + /** + * @brief Clean up engine resources. + */ + void Cleanup(); + + /** + * @brief Create a new entity. + * @param name The name of the entity. + * @return A pointer to the newly created entity. + */ + Entity *CreateEntity(const std::string &name); + + /** + * @brief Get an entity by name. + * @param name The name of the entity. + * @return A pointer to the entity, or nullptr if not found. + */ + Entity *GetEntity(const std::string &name); + + /** + * @brief Get all entities. + * @return A const reference to the vector of entities. + */ + const std::vector> &GetEntities() const + { + return entities; + } + + /** + * @brief Remove an entity. + * @param entity The entity to remove. + * @return True if the entity was removed, false otherwise. + */ + bool RemoveEntity(Entity *entity); + + /** + * @brief Remove an entity by name. + * @param name The name of the entity to remove. + * @return True if the entity was removed, false otherwise. + */ + bool RemoveEntity(const std::string &name); + + /** + * @brief Set the active camera. + * @param cameraComponent The camera component to set as active. + */ + void SetActiveCamera(CameraComponent *cameraComponent); + + /** + * @brief Get the active camera. + * @return A pointer to the active camera component, or nullptr if none is set. + */ + const CameraComponent *GetActiveCamera() const; + + /** + * @brief Get the resource manager. + * @return A pointer to the resource manager. + */ + const ResourceManager *GetResourceManager() const; + + /** + * @brief Get the platform. + * @return A pointer to the platform. + */ + const Platform *GetPlatform() const; + + /** + * @brief Get the renderer. + * @return A pointer to the renderer. + */ + Renderer *GetRenderer(); + + /** + * @brief Get the model loader. + * @return A pointer to the model loader. + */ + ModelLoader *GetModelLoader(); + + /** + * @brief Get the audio system. + * @return A pointer to the audio system. + */ + const AudioSystem *GetAudioSystem() const; + + /** + * @brief Get the physics system. + * @return A pointer to the physics system. + */ + PhysicsSystem *GetPhysicsSystem(); + + /** + * @brief Get the ImGui system. + * @return A pointer to the ImGui system. + */ + const ImGuiSystem *GetImGuiSystem() const; + + /** + * @brief Handles mouse input for interaction and camera control. + * + * This method processes mouse input for various functionalities, including interacting with the scene, + * camera rotation, and delegating handling to ImGui or hover systems. + * + * @param x The x-coordinate of the mouse position. + * @param y The y-coordinate of the mouse position. + * @param buttons A bitmask representing the state of mouse buttons. + * Bit 0 corresponds to the left button, and Bit 1 corresponds to the right button. + */ + void handleMouseInput(float x, float y, uint32_t buttons); + + /** + * @brief Handles keyboard input events for controlling the camera and other subsystems. + * + * This method processes key press and release events to update the camera's movement state. + * It also forwards the input to other subsystems like the ImGui interface if applicable. + * + * @param key The key code of the keyboard input. + * @param pressed Indicates whether the key is pressed (true) or released (false). + */ + void handleKeyInput(uint32_t key, bool pressed); + +#if defined(PLATFORM_ANDROID) +/** + * @brief Initialize the engine for Android. + * @param app The Android app. + * @param appName The name of the application. + * @param enableValidationLayers Whether to enable Vulkan validation layers. + * @return True if initialization was successful, false otherwise. + */ +# if defined(NDEBUG) + bool InitializeAndroid(android_app *app, const std::string &appName, bool enableValidationLayers = false, bool useXR = false); +# else + bool InitializeAndroid(android_app *app, const std::string &appName, bool enableValidationLayers = true, bool useXR = false); +# endif + + /** + * @brief Run the engine on Android. + */ + void RunAndroid(); +#endif + + private: + // Subsystems + std::unique_ptr platform; + std::unique_ptr renderer; + std::unique_ptr resourceManager; + std::unique_ptr modelLoader; + std::unique_ptr audioSystem; + std::unique_ptr physicsSystem; + std::unique_ptr imguiSystem; + + // Entities + // NOTE: Entities can be created from a background loading thread (see `main.cpp`). + // Protect the containers to avoid iterator invalidation/data races while the render thread + // iterates them. + mutable std::shared_mutex entitiesMutex; + std::vector> entities; + std::unordered_map entityMap; + + // Main thread identity (used to defer destructive operations from background threads) + std::thread::id mainThreadId{}; + + // Background threads may request entity removal while the render thread is iterating snapshots. + // To keep `Entity*` snapshots safe, defer removals to the main thread at a safe point. + std::mutex pendingEntityRemovalsMutex; + std::vector pendingEntityRemovalNames; + void ProcessPendingEntityRemovals(); + bool IsMainThread() const; + void UpdateFPSWindowTitle(); + + // Active camera + CameraComponent *activeCamera = nullptr; + + // Engine state + bool initialized = false; + bool running = false; + + // Delta time calculation + // deltaTimeMs: time since last frame in milliseconds (for clarity) + std::chrono::milliseconds deltaTimeMs{0}; + uint64_t lastFrameTimeMs = 0; + + // Frame counter and FPS calculation + uint64_t frameCount = 0; + float fpsUpdateTimer = 0.0f; + float currentFPS = 0.0f; + uint64_t lastFPSUpdateFrame = 0; + + // Camera control state + struct CameraControlState + { + bool moveForward = false; + bool moveBackward = false; + bool moveLeft = false; + bool moveRight = false; + bool moveUp = false; + bool moveDown = false; + bool mouseLeftPressed = false; + bool mouseRightPressed = false; + float lastMouseX = 0.0f; + float lastMouseY = 0.0f; + float yaw = 0.0f; + float pitch = 0.0f; + bool firstMouse = true; + float cameraSpeed = 5.0f; + float mouseSensitivity = 0.1f; + bool baseOrientationCaptured = false; + glm::quat baseOrientation{1.0f, 0.0f, 0.0f, 0.0f}; + } cameraControl; + + // Mouse position tracking + float currentMouseX = 0.0f; + float currentMouseY = 0.0f; + + // Ball material properties for PBR + struct BallMaterial + { + glm::vec3 albedo; + float metallic; + float roughness; + float ao; + glm::vec3 emissive; + float bounciness; + }; + + BallMaterial ballMaterial; + + // Physics scaling configuration + // The bistro scene spans roughly 20 game units and represents a realistic cafe/bistro space + // Based on issue feedback: game units should NOT equal 1m and need proper scaling + // Analysis shows bistro geometry pieces are much smaller than assumed + struct PhysicsScaling + { + float gameUnitsToMeters = 0.1f; // 1 game unit = 0.1 meter (10cm) - more realistic scale + float physicsTimeScale = 1.0f; // Normal time scale for stable physics + float forceScale = 2.0f; // Much reduced force scaling for visual gameplay (was 10.0f) + float gravityScale = 0.1f; // Scaled gravity for smaller world scale + }; + + PhysicsScaling physicsScaling; + + // Pending ball creation data + struct PendingBall + { + glm::vec3 spawnPosition; + glm::vec3 throwDirection; + float throwForce; + glm::vec3 randomSpin; + std::string ballName; + }; + + std::vector pendingBalls; + + /** + * @brief Update the engine state. + * @param deltaTime The time elapsed since the last update. + */ + // Accepts a time delta in milliseconds for clarity + void Update(TimeDelta deltaTime); + + /** + * @brief Update the engine state for XR. + * @param predictedTime The predicted display time from OpenXR. + */ + void Update(XrTime predictedTime); + + /** + * @brief Render the scene. + */ + void Render(); + + /** + * @brief Render the scene for XR. + * @param predictedTime The predicted display time from OpenXR. + */ + void Render(XrTime predictedTime); + + /** + * @brief Calculate the time delta between frames. + * @return The delta time in milliseconds (steady_clock based). + */ + std::chrono::milliseconds CalculateDeltaTimeMs(); + + /** + * @brief Handle window resize events. + * @param width The new width of the window. + * @param height The new height of the window. + */ + void HandleResize(int width, int height) const; + + /** + * @brief Update camera controls based on input state. + * @param deltaTime The time elapsed since the last update. + */ + void UpdateCameraControls(TimeDelta deltaTime); + + /** + * @brief Generate random PBR material properties for the ball. + */ + void GenerateBallMaterial(); + + /** + * @brief Initialize physics scaling based on scene analysis. + */ + void InitializePhysicsScaling(); + + /** + * @brief Convert a force value from game units to physics units. + * @param gameForce Force in game units. + * @return Force scaled for physics simulation. + */ + float ScaleForceForPhysics(float gameForce) const; + + /** + * @brief Convert gravity from real-world units to game physics units. + * @param realWorldGravity Gravity in m/s². + * @return Gravity scaled for game physics. + */ + glm::vec3 ScaleGravityForPhysics(const glm::vec3 &realWorldGravity) const; + + /** + * @brief Convert time delta for physics simulation. + * @param deltaTime Real delta time. + * @return Scaled delta time for physics. + */ + float ScaleTimeForPhysics(float deltaTime) const; + + /** + * @brief Throw a ball into the scene with random properties. + * @param mouseX The x-coordinate of the mouse click. + * @param mouseY The y-coordinate of the mouse click. + */ + void ThrowBall(float mouseX, float mouseY); + + /** + * @brief Process pending ball creations outside the rendering loop. + */ + void ProcessPendingBalls(); + + /** + * @brief Handle mouse hover to track current mouse position. + * @param mouseX The x-coordinate of the mouse position. + * @param mouseY The y-coordinate of the mouse position. + */ + void HandleMouseHover(float mouseX, float mouseY); +}; diff --git a/attachments/openxr_engine/install_dependencies_linux.sh b/attachments/openxr_engine/install_dependencies_linux.sh new file mode 100755 index 000000000..276fb42f8 --- /dev/null +++ b/attachments/openxr_engine/install_dependencies_linux.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SIMPLE_ENGINE_DIR="$SCRIPT_DIR/../simple_engine" + +if [ ! -d "$SIMPLE_ENGINE_DIR" ]; then + echo "Error: simple_engine directory not found at $SIMPLE_ENGINE_DIR" + exit 1 +fi + +# Run simple engine dependencies first +# We pass the work root to avoid conflicts if needed, but the script uses a fixed one anyway. +bash "$SIMPLE_ENGINE_DIR/install_dependencies_linux.sh" + +# Install OpenXR +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME +fi + +echo "Installing OpenXR dependencies for $OS..." + +case "$OS" in + "Ubuntu"* | "Debian"* | "Linux Mint"*) + sudo apt-get update + sudo apt-get install -y libopenxr-dev + ;; + "Fedora"* | "Red Hat"* | "CentOS"* | "Rocky"*) + sudo dnf install -y openxr-devel + ;; + "Arch"* | "Manjaro"*) + sudo pacman -S --noconfirm openxr + ;; + *) + echo "Warning: Unsupported OS for automatic OpenXR installation. Please install OpenXR SDK manually." + ;; +esac + +echo "OpenXR dependencies installation completed!" diff --git a/attachments/openxr_engine/install_dependencies_windows.bat b/attachments/openxr_engine/install_dependencies_windows.bat new file mode 100644 index 000000000..6d46f0532 --- /dev/null +++ b/attachments/openxr_engine/install_dependencies_windows.bat @@ -0,0 +1,19 @@ +@echo off +set SCRIPT_DIR=%~dp0 +set SIMPLE_ENGINE_DIR=%SCRIPT_DIR%..\simple_engine + +if not exist "%SIMPLE_ENGINE_DIR%" ( + echo Error: simple_engine directory not found at %SIMPLE_ENGINE_DIR% + exit /b 1 +) + +echo Calling simple_engine dependencies installer... +call "%SIMPLE_ENGINE_DIR%\install_dependencies_windows.bat" + +echo. +echo Installing OpenXR loader... +vcpkg install openxr-loader --triplet=x64-windows + +echo. +echo OpenXR dependencies installation completed! +exit /b 0 diff --git a/attachments/openxr_engine/main.cpp b/attachments/openxr_engine/main.cpp new file mode 100644 index 000000000..ee8224086 --- /dev/null +++ b/attachments/openxr_engine/main.cpp @@ -0,0 +1,157 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "camera_component.h" +#include "crash_reporter.h" +#include "engine.h" +#include "scene_loading.h" +#include "transform_component.h" +#include "xr_context.h" + +#include +#include +#include + +// Constants +constexpr int WINDOW_WIDTH = 800; +constexpr int WINDOW_HEIGHT = 600; +#if defined(NDEBUG) +constexpr bool ENABLE_VALIDATION_LAYERS = false; +#else +constexpr bool ENABLE_VALIDATION_LAYERS = true; +#endif + +/** + * @brief Set up a simple scene with a camera and some objects. + * @param engine The engine to set up the scene in. + */ +void SetupScene(Engine *engine) +{ + // Create a camera entity + Entity *cameraEntity = engine->CreateEntity("Camera"); + if (!cameraEntity) + { + throw std::runtime_error("Failed to create camera entity"); + } + + // Add a transform component to the camera + auto *cameraTransform = cameraEntity->AddComponent(); + cameraTransform->SetPosition(glm::vec3(0.0f, 0.0f, 3.0f)); + + // Add a camera component to the camera entity + auto *camera = cameraEntity->AddComponent(); + camera->SetAspectRatio(static_cast(WINDOW_WIDTH) / static_cast(WINDOW_HEIGHT)); + + // Set the camera as the active camera + engine->SetActiveCamera(camera); + + // Kick off GLTF model loading on a background thread so the main loop + // can start and render the UI/progress bar while the scene is being + // constructed. Engine::Update will avoid updating entities while + // loading is in progress to prevent data races. + if (auto *renderer = engine->GetRenderer()) + { + renderer->SetLoading(true); + renderer->SetLoadingPhase(Renderer::LoadingPhase::Textures); + } + std::thread([engine] { + LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + }).detach(); +} + +#if defined(PLATFORM_ANDROID) +/** + * @brief Android entry point. + * @param app The Android app. + */ +void android_main(android_app *app) +{ + try + { + // Create the engine + Engine engine; + + // Test if we are able to run in XR mode + bool useXR = false; + if (XrContext::checkRuntimeAvailable()) { + std::cout << "OpenXR Runtime detected. Attempting spatial initialization..." << std::endl; + useXR = true; + } + + // Initialize the engine + if (!engine.InitializeAndroid(app, "Simple Engine", ENABLE_VALIDATION_LAYERS, useXR)) + { + throw std::runtime_error("Failed to initialize engine"); + } + + // Set up the scene + SetupScene(&engine); + + // Run the engine + engine.RunAndroid(); + } + catch (const std::exception &e) + { + LOGE("Exception: %s", e.what()); + } +} +#else +/** + * @brief Desktop entry point. + * @return The exit code. + */ +int main(int, char *[]) +{ + try + { + // Enable minidump generation for Release-only crashes (e.g., stack cookie failures / fast-fail). + // Writes dumps under the current working directory (the build/run directory). + CrashReporter::GetInstance().Initialize("crashes", "SimpleEngine", "1.0.0"); + + // Create the engine + Engine engine; + + // Test if we are able to run in XR mode + bool useXR = false; + if (XrContext::checkRuntimeAvailable()) { + std::cout << "OpenXR Runtime detected. Attempting spatial initialization..." << std::endl; + useXR = true; + } + + // Initialize the engine + if (!engine.Initialize("Simple Engine", WINDOW_WIDTH, WINDOW_HEIGHT, ENABLE_VALIDATION_LAYERS, useXR)) + { + throw std::runtime_error("Failed to initialize engine"); + } + + // Set up the scene + SetupScene(&engine); + + // Run the engine + engine.Run(); + + CrashReporter::GetInstance().Cleanup(); + + return 0; + } + catch (const std::exception &e) + { + std::cerr << "Exception: " << e.what() << std::endl; + CrashReporter::GetInstance().Cleanup(); + return 1; + } +} +#endif diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h new file mode 100644 index 000000000..a5913c200 --- /dev/null +++ b/attachments/openxr_engine/renderer.h @@ -0,0 +1,1932 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xr_context.h" +#include "camera_component.h" +#include "entity.h" +#include "memory_pool.h" +#include "mesh_component.h" +#include "model_loader.h" +#include "platform.h" +#include "thread_pool.h" + +// Fallback defines for optional extension names (allow compiling against older headers) +#ifndef VK_EXT_ROBUSTNESS_2_EXTENSION_NAME +# define VK_EXT_ROBUSTNESS_2_EXTENSION_NAME "VK_EXT_robustness2" +#endif +#ifndef VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME +# define VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME "VK_KHR_dynamic_rendering_local_read" +#endif +#ifndef VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME +# define VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME "VK_EXT_shader_tile_image" +#endif + +// Forward declarations +class ImGuiSystem; + +/** + * @brief Structure for Vulkan queue family indices. + */ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + std::optional transferFamily; // optional dedicated transfer queue family + + [[nodiscard]] bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); + } +}; + +/** + * @brief Structure for swap chain support details. + */ +struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; + +/** + * @brief Structure for individual light data in the storage buffer. + */ +struct LightData { + alignas(16) glm::vec4 position; // Light position (w component used for direction vs position) + alignas(16) glm::vec4 color; // Light color and intensity + alignas(16) glm::mat4 lightSpaceMatrix; // Light space matrix for shadow mapping + alignas(16) glm::vec4 direction; // Light direction (for directional/spotlights) + alignas(4) int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive + alignas(4) float range; // Light range + alignas(4) float innerConeAngle; // For spotlights + alignas(4) float outerConeAngle; // For spotlights +}; + +struct ShadowUniforms { + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; + +struct ShadowPushConstants { + alignas(16) glm::mat4 model; +}; + +/** + * @brief Structure for the uniform buffer object (now without fixed light arrays). + */ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 views[4]; // Supporting up to 4 views (stereo + quad views) + alignas(16) glm::mat4 projs[4]; + alignas(16) glm::vec4 camPoses[4]; + alignas(4) float exposure; + alignas(4) float gamma; + alignas(4) float prefilteredCubeMipLevels; + alignas(4) float scaleIBLAmbient; + alignas(4) int lightCount; + alignas(4) int padding0; // match shader UBO layout + alignas(4) float padding1; // match shader UBO layout + alignas(4) float padding2; // match shader UBO layout + alignas(8) glm::vec2 screenDimensions; + alignas(4) float nearZ; + alignas(4) float farZ; + alignas(4) float slicesZ; + alignas(4) float _uboPad3; + // Planar reflections + alignas(16) glm::mat4 reflectionVP; // projection * mirroredView + alignas(4) int reflectionEnabled; // 1 when sampling reflection in main pass + alignas(4) int reflectionPass; // 1 during reflection render pass + alignas(8) glm::vec2 _reflectPad0; + alignas(16) glm::vec4 clipPlaneWS; // world-space plane ax+by+cz+d=0 + // Controls + alignas(4) float reflectionIntensity; // scales reflection mix in glass + alignas(4) int enableRayQueryReflections = 1; // 1 to enable reflections in ray query mode + alignas(4) int enableRayQueryTransparency = 1; // 1 to enable transparency/refraction in ray query mode + alignas(4) float _padReflect[1]{}; + // Ray-query specific: number of per-instance geometry infos in buffer + alignas(4) int geometryInfoCount{0}; + alignas(4) int _padGeo0{0}; + alignas(4) int _padGeo1{0}; + alignas(4) int _padGeo2{0}; + alignas(16) glm::vec4 _rqReservedWorldPos{0.0f, 0.0f, 0.0f, 0.0f}; + // Ray-query specific: number of materials in materialBuffer + alignas(4) int materialCount{0}; + alignas(4) int _padMat0{0}; + alignas(4) int _padMat1{0}; + alignas(4) int _padMat2{0}; +}; + +// Ray Query uses a dedicated uniform buffer with its own tightly-defined layout. +// This avoids relying on the (much larger) shared raster UBO layout and prevents +// CPU↔shader layout drift from breaking Ray Query-only fields. +// +// IMPORTANT: This layout must match `RayQueryUniforms` in `shaders/ray_query.slang`. +struct RayQueryUniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + alignas(16) glm::vec4 camPos; + + alignas(4) float exposure; + alignas(4) float gamma; + // Match raster UBO conventions so Ray Query can run the same lighting math. + alignas(4) float scaleIBLAmbient; + alignas(4) int lightCount; + alignas(4) int enableRayQueryReflections; + alignas(4) int enableRayQueryTransparency; + + alignas(8) glm::vec2 screenDimensions; + alignas(4) int geometryInfoCount; + alignas(4) int materialCount; + alignas(4) int _pad0; // used for rayQueryMaxBounces + // Thick-glass controls (RQ-only) + alignas(4) int enableThickGlass; // 0/1 toggle + alignas(4) float thicknessClamp; // max thickness in meters + alignas(4) float absorptionScale; // scales sigma_a + alignas(4) int _pad1; // Ray Query: enable hard shadows for direct lighting (0/1) + // Ray Query soft shadows (area-light approximation) + alignas(4) int shadowSampleCount; // 1 = hard shadows; >1 = multi-sample + alignas(4) float shadowSoftness; // 0 = hard; otherwise scales effective light radius (fraction of range) + alignas(4) float reflectionIntensity; // User control for glass reflection strength + alignas(4) float _padShadow[2]{}; +}; + +static_assert(sizeof(RayQueryUniformBufferObject) == 288, "RayQueryUniformBufferObject size must match shader layout"); +static_assert(offsetof(RayQueryUniformBufferObject, model) == 0); +static_assert(offsetof(RayQueryUniformBufferObject, view) == 64); +static_assert(offsetof(RayQueryUniformBufferObject, proj) == 128); +static_assert(offsetof(RayQueryUniformBufferObject, camPos) == 192); +static_assert(offsetof(RayQueryUniformBufferObject, exposure) == 208); +static_assert(offsetof(RayQueryUniformBufferObject, gamma) == 212); +static_assert(offsetof(RayQueryUniformBufferObject, scaleIBLAmbient) == 216); +static_assert(offsetof(RayQueryUniformBufferObject, lightCount) == 220); +static_assert(offsetof(RayQueryUniformBufferObject, enableRayQueryReflections) == 224); +static_assert(offsetof(RayQueryUniformBufferObject, enableRayQueryTransparency) == 228); +static_assert(offsetof(RayQueryUniformBufferObject, screenDimensions) == 232); +static_assert(offsetof(RayQueryUniformBufferObject, geometryInfoCount) == 240); +static_assert(offsetof(RayQueryUniformBufferObject, materialCount) == 244); +static_assert(offsetof(RayQueryUniformBufferObject, _pad0) == 248); +static_assert(offsetof(RayQueryUniformBufferObject, enableThickGlass) == 252); +static_assert(offsetof(RayQueryUniformBufferObject, thicknessClamp) == 256); +static_assert(offsetof(RayQueryUniformBufferObject, absorptionScale) == 260); +static_assert(offsetof(RayQueryUniformBufferObject, _pad1) == 264); +static_assert(offsetof(RayQueryUniformBufferObject, shadowSampleCount) == 268); +static_assert(offsetof(RayQueryUniformBufferObject, shadowSoftness) == 272); + +/** + * @brief Structure for PBR material properties. + * This structure must match the PushConstants structure in the PBR shader. + */ +struct MaterialProperties { + alignas(16) glm::vec4 baseColorFactor; + alignas(4) float metallicFactor; + alignas(4) float roughnessFactor; + alignas(4) int baseColorTextureSet; + alignas(4) int physicalDescriptorTextureSet; + alignas(4) int normalTextureSet; + alignas(4) int occlusionTextureSet; + alignas(4) int emissiveTextureSet; + alignas(4) float alphaMask; + alignas(4) float alphaMaskCutoff; + alignas(16) glm::vec3 emissiveFactor; // Emissive factor for HDR emissive sources + alignas(4) float emissiveStrength; // KHR_materials_emissive_strength extension + alignas(4) float transmissionFactor; // KHR_materials_transmission + alignas(4) int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness + alignas(4) float glossinessFactor; // SpecGloss glossiness scalar + alignas(16) glm::vec3 specularFactor; // SpecGloss specular color factor + alignas(4) float ior = 1.5f; // index of refraction + alignas(4) bool hasEmissiveStrengthExtension; +}; + +/** + * @brief Rendering mode selection + */ +enum class RenderMode { + Rasterization, // Traditional rasterization pipeline + RayQuery // Ray query compute shader +}; + +/** + * @brief Class for managing Vulkan rendering. + * + * This class implements the rendering pipeline as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc + */ +class Renderer { + public: + /** + * @brief Constructor with a platform. + * @param platform The platform to use for rendering. + */ + explicit Renderer(Platform* platform); + + /** + * @brief Destructor for proper cleanup. + */ + ~Renderer(); + + /** + * @brief Initialize the renderer. + * @param appName The name of the application. + * @param enableValidationLayers Whether to enable validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, bool enableValidationLayers = true, bool useXR = false); + + /** + * @brief Clean up renderer resources. + */ + void Cleanup(); + + /** + * @brief Render the scene. + * @param entities The entities to render. + * @param camera The camera to use for rendering. + * @param imguiSystem The ImGui system for UI rendering (optional). + */ + void Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem = nullptr); + + // Render overload that accepts a snapshot of raw entity pointers. + // This allows the Engine to release its entity-container lock before rendering + // (avoiding writer starvation of background loading/physics threads). + void Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem = nullptr); + void Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem, XrTime predictedTime); + + /** + * @brief Wait for the device to be idle. + */ + void WaitIdle(); + + /** + * @brief Wait for fences with periodic watchdog kicks to prevent false hang detection. + * Must be called from the render thread. + */ + vk::Result waitForFencesSafe(const std::vector& fences, vk::Bool32 waitAll, uint64_t timeoutNs = 100'000'000ULL); + + /** + * @brief Wait for fences with periodic watchdog kicks to prevent false hang detection. + * Must be called from the render thread. Overload for a single fence. + */ + vk::Result waitForFencesSafe(vk::Fence fence, vk::Bool32 waitAll, uint64_t timeoutNs = 100'000'000ULL); + + /** + * @brief Dispatch a compute shader. + * @param groupCountX The number of local workgroups to dispatch in the X dimension. + * @param groupCountY The number of local workgroups to dispatch in the Y dimension. + * @param groupCountZ The number of local workgroups to dispatch in the Z dimension. + * @param inputBuffer The input buffer. + * @param outputBuffer The output buffer. + * @param hrtfBuffer The HRTF data buffer. + * @param paramsBuffer The parameters buffer. + * @return A fence that can be used to synchronize with the compute operation. + */ + vk::raii::Fence DispatchCompute(uint32_t groupCountX, + uint32_t groupCountY, + uint32_t groupCountZ, + vk::Buffer inputBuffer, + vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, + vk::Buffer paramsBuffer); + + /** + * @brief Check if the renderer is initialized. + * @return True if the renderer is initialized, false otherwise. + */ + bool IsInitialized() const { + return initialized; + } + + /** + * @brief Get the Vulkan device. + * @return The Vulkan device. + */ + vk::Device GetDevice() const { + return *device; + } + + // Expose max frames in flight for per-frame resource duplication + uint32_t GetMaxFramesInFlight() const { + return MAX_FRAMES_IN_FLIGHT; + } + + /** + * @brief Get the Vulkan RAII device. + * @return The Vulkan RAII device. + */ + const vk::raii::Device& GetRaiiDevice() const { + return device; + } + +#if defined(PLATFORM_ANDROID) + void SetAndroidApp(struct android_app* app) { xrContext.setAndroidApp(app); } +#endif + + // Expose uploads timeline semaphore and last value for external waits + vk::Semaphore GetUploadsTimelineSemaphore() const { + return *uploadsTimeline; + } + uint64_t GetUploadsTimelineValue() const { + return uploadTimelineLastSubmitted.load(std::memory_order_relaxed); + } + + /** + * @brief Get the compute queue. + * @return The compute queue. + */ + vk::Queue GetComputeQueue() const { + std::lock_guard lock(queueMutex); + return *computeQueue; + } + + /** + * @brief Find a suitable memory type. + * @param typeFilter The type filter. + * @param properties The memory properties. + * @return The memory type index. + */ + uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + return findMemoryType(typeFilter, properties); + } + + /** + * @brief Get the compute queue family index. + * @return The compute queue family index. + */ + uint32_t GetComputeQueueFamilyIndex() const { + if (queueFamilyIndices.computeFamily.has_value()) { + return queueFamilyIndices.computeFamily.value(); + } + // Fallback to graphics family to avoid crashes on devices without a separate compute queue + return queueFamilyIndices.graphicsFamily.value(); + } + + /** + * @brief Submit a command buffer to the compute queue with proper dispatch loader preservation. + * @param commandBuffer The command buffer to submit. + * @param fence The fence to signal when the operation completes. + */ + void SubmitToComputeQueue(vk::CommandBuffer commandBuffer, vk::Fence fence) const { + // Use mutex to ensure thread-safe access to queues + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer + }; + std::lock_guard lock(queueMutex); + // Prefer compute queue when available; otherwise, fall back to graphics queue to avoid crashes + if (*computeQueue) { + computeQueue.submit(submitInfo, fence); + } else { + graphicsQueue.submit(submitInfo, fence); + } + } + + /** + * @brief Create a shader module from SPIR-V code. + * @param code The SPIR-V code. + * @return The shader module. + */ + vk::raii::ShaderModule CreateShaderModule(const std::vector& code) { + return createShaderModule(code); + } + + /** + * @brief Create a shader module from a file. + * @param filename The filename. + * @return The shader module. + */ + vk::raii::ShaderModule CreateShaderModule(const std::string& filename) { + auto code = readFile(filename); + return createShaderModule(code); + } + + /** + * @brief Load a texture from a file. + * @param texturePath The path to the texture file. + * @return True if the texture was loaded successfully, false otherwise. + */ + bool LoadTexture(const std::string& texturePath); + + // Asynchronous texture loading APIs (thread-pool backed). + // The 'critical' flag is used to front-load important textures (e.g., + // baseColor/albedo) so the scene looks mostly correct before the loading + // screen disappears. Non-critical textures (normals, MR, AO, emissive) + // can stream in after geometry is visible. + std::future LoadTextureAsync(const std::string& texturePath, bool critical = false); + + /** + * @brief Load a texture from raw image data in memory. + * @param textureId The identifier for the texture. + * @param imageData The raw image data. + * @param width The width of the image. + * @param height The height of the image. + * @param channels The number of channels in the image. + * @return True if the texture was loaded successfully, false otherwise. + */ + bool LoadTextureFromMemory(const std::string& textureId, + const unsigned char* imageData, + int width, + int height, + int channels); + + // Asynchronous upload from memory (RGBA/RGB/other). Safe for concurrent calls. + std::future LoadTextureFromMemoryAsync(const std::string& textureId, + const unsigned char* imageData, + int width, + int height, + int channels, + bool critical = false); + + // Progress query for UI + uint32_t GetTextureTasksScheduled() const { + return textureTasksScheduled.load(); + } + uint32_t GetTextureTasksCompleted() const { + return textureTasksCompleted.load(); + } + + // GPU upload progress (per-texture jobs processed on the main thread). + uint32_t GetUploadJobsTotal() const { + return uploadJobsTotal.load(); + } + uint32_t GetUploadJobsCompleted() const { + return uploadJobsCompleted.load(); + } + + // --- Acceleration structure build progress (for UI) --- + // Exposed so the loading overlay can show meaningful progress when + // BLAS/TLAS builds take a long time (>= ~10 seconds). + bool IsASBuildInProgress() const { + return asBuildUiActive.load(std::memory_order_relaxed); + } + float GetASBuildProgress() const { + return asBuildUiProgress.load(std::memory_order_relaxed); + } + uint32_t GetASBuildItemsDone() const { + return asBuildUiDone.load(std::memory_order_relaxed); + } + uint32_t GetASBuildItemsTotal() const { + return asBuildUiTotal.load(std::memory_order_relaxed); + } + const char* GetASBuildStage() const { + return asBuildUiStage.load(std::memory_order_relaxed); + } + double GetASBuildElapsedSeconds() const { + const uint64_t start = asBuildUiStartNs.load(std::memory_order_relaxed); + if (start == 0) + return 0.0; + const uint64_t now = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + if (now <= start) + return 0.0; + return static_cast(now - start) / 1'000'000'000.0; + } + bool ShouldShowASBuildProgressInUI() const { + return IsASBuildInProgress() && GetASBuildElapsedSeconds() >= 10.0; + } + + // Block until all currently-scheduled texture tasks have completed. + // Intended for use during initial scene loading so that descriptor + // creation sees the final textureResources instead of fallbacks. + void WaitForAllTextureTasks(); + + // Process pending texture GPU uploads on the calling thread. + // This should be invoked from the main/render thread so that all + // Vulkan work happens from a single thread while worker threads + // perform only CPU-side decoding. + // + // Parameters allow us to: + // - limit the number of jobs processed per call (for streaming), and + // - choose whether to include critical and/or non-critical jobs. + void ProcessPendingTextureJobs(uint32_t maxJobs = UINT32_MAX, + bool includeCritical = true, + bool includeNonCritical = true); + + // Track which entities use a given texture ID so that descriptor sets + // can be refreshed when textures finish streaming in. + void RegisterTextureUser(const std::string& textureId, Entity* entity); + void OnTextureUploaded(const std::string& textureId); + + // Global loading state (model/scene). Consider the scene "loading" while + // either the model is being parsed/instantiated OR there are still + // outstanding critical texture uploads (e.g., baseColor/albedo). + // Loading state: show blocking loading overlay only until the initial scene is ready. + // Background streaming may continue after that without blocking the scene. + enum class LoadingPhase : uint32_t { + Scene = 0, + Textures, + Physics, + AccelerationStructures, + Finalizing + }; + LoadingPhase GetLoadingPhase() const { + return static_cast(loadingPhase.load(std::memory_order_relaxed)); + } + const char* GetLoadingPhaseName() const { + switch (GetLoadingPhase()) { + case LoadingPhase::Scene: + return "Scene"; + case LoadingPhase::Textures: + return "Textures"; + case LoadingPhase::Physics: + return "Physics"; + case LoadingPhase::AccelerationStructures: + return "Acceleration Structures"; + case LoadingPhase::Finalizing: + return "Finalizing"; + default: + return "Loading"; + } + } + float GetLoadingPhaseProgress() const { + return std::clamp(loadingPhaseProgress.load(std::memory_order_relaxed), 0.0f, 1.0f); + } + void SetLoadingPhase(LoadingPhase phase) { + loadingPhase.store(static_cast(phase), std::memory_order_relaxed); + loadingPhaseProgress.store(0.0f, std::memory_order_relaxed); + } + void SetLoadingPhaseProgress(float v) { + loadingPhaseProgress.store(std::clamp(v, 0.0f, 1.0f), std::memory_order_relaxed); + } + void MarkInitialLoadComplete() { + initialLoadComplete.store(true, std::memory_order_relaxed); + SetLoadingPhase(LoadingPhase::Finalizing); + loadingPhaseProgress.store(1.0f, std::memory_order_relaxed); + } + bool IsLoading() const { + // Keep the blocking overlay visible until the engine has finished + // post-load blockers (AS build, descriptor cold-init, etc.). + return (loadingFlag.load(std::memory_order_relaxed) || criticalJobsOutstanding.load(std::memory_order_relaxed) > 0u || + !initialLoadComplete.load(std::memory_order_relaxed)); + } + // True only while the model/scene is still being constructed or while critical + // texture jobs remain outstanding. This excludes the "finalizing" stage where + // the render thread may still be doing post-load work (AS build, descriptor init). + // + // IMPORTANT: Do NOT use critical texture completion as a gate for starting TLAS/BLAS builds. + // AS builds depend on geometry buffers and instance transforms, not on texture readiness. + bool IsSceneLoaderActive() const { + return loadingFlag.load(std::memory_order_relaxed); + } + void SetLoading(bool v) { + loadingFlag.store(v, std::memory_order_relaxed); + if (v) { + // New load cycle starting + initialLoadComplete.store(false, std::memory_order_relaxed); + SetLoadingPhase(LoadingPhase::Scene); + } + } + + // Descriptor set deferred update machinery + void MarkEntityDescriptorsDirty(Entity *entity); + void ProcessDirtyDescriptorsForFrame(uint32_t frameIndex); + + // Texture aliasing: map canonical IDs to actual loaded keys (e.g., file paths) to avoid duplicates + inline void RegisterTextureAlias(const std::string& aliasId, const std::string& targetId) { + std::unique_lock lock(textureResourcesMutex); + if (aliasId.empty() || targetId.empty()) + return; + // Resolve targetId without re-locking by walking the alias map directly + std::string resolved = targetId; + for (int i = 0; i < 8; ++i) { + auto it = textureAliases.find(resolved); + if (it == textureAliases.end()) + break; + if (it->second == resolved) + break; + resolved = it->second; + } + if (aliasId == resolved) { + textureAliases.erase(aliasId); + } else { + textureAliases[aliasId] = resolved; + } + } + inline std::string ResolveTextureId(const std::string& id) const { + std::shared_lock lock(textureResourcesMutex); + std::string cur = id; + for (int i = 0; i < 8; ++i) { + // prevent pathological cycles + auto it = textureAliases.find(cur); + if (it == textureAliases.end()) + break; + if (it->second == cur) + break; // self-alias guard + cur = it->second; + } + return cur; + } + + /** + * @brief Transition an image layout. + * @param image The image. + * @param format The image format. + * @param oldLayout The old layout. + * @param newLayout The new layout. + */ + void TransitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { + transitionImageLayout(image, format, oldLayout, newLayout); + } + + /** + * @brief Copy a buffer to an image. + * @param buffer The buffer. + * @param image The image. + * @param width The image width. + * @param height The image height. + */ + void CopyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height) { + // Create a default single region for backward compatibility + std::vector regions = { + { + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + } + }; + copyBufferToImage(buffer, image, width, height, regions); + } + + /** + * @brief Get the current command buffer. + * @return The current command buffer. + */ + vk::raii::CommandBuffer& GetCurrentCommandBuffer() { + return commandBuffers[currentFrame]; + } + + /** + * @brief Get the swap chain image format. + * @return The swap chain image format. + */ + vk::Format GetSwapChainImageFormat() const { + return swapChainImageFormat; + } + + /** + * @brief Set the framebuffer resized flag. + * This should be called when the window is resized to trigger swap chain recreation. + */ + void SetFramebufferResized() { + framebufferResized.store(true, std::memory_order_relaxed); + } + + /** + * @brief Set the model loader reference for accessing extracted lights. + * @param _modelLoader Pointer to the model loader. + */ + void SetModelLoader(ModelLoader* _modelLoader) { + modelLoader = _modelLoader; + // Materials are resolved via ModelLoader; invalidate cached per-entity material info. + for (auto& kv : entityResources) { + kv.second.materialCacheValid = false; + kv.second.cachedMaterial = nullptr; + kv.second.cachedIsBlended = false; + kv.second.cachedIsGlass = false; + kv.second.cachedIsLiquid = false; + kv.second.cachedMaterialProps = MaterialProperties{}; + } + } + + /** + * @brief Set static lights loaded during model initialization. + * @param lights The lights to store statically. + */ + void SetStaticLights(const std::vector& lights) { + staticLights = lights; + std::cout << "[Lights] staticLights set: " << staticLights.size() << " entries" << std::endl; + } + + /** + * @brief Set the gamma correction value for PBR rendering. + * @param _gamma The gamma correction value (typically 2.2). + */ + void SetGamma(float _gamma) { + gamma = _gamma; + } + + /** + * @brief Set the exposure value for HDR tone mapping. + * @param _exposure The exposure value (1.0 = no adjustment). + */ + void SetExposure(float _exposure) { + exposure = _exposure; + } + + // Reflection intensity (UI + shader control) + void SetReflectionIntensity(float v) { + reflectionIntensity = v; + } + float GetReflectionIntensity() const { + return reflectionIntensity; + } + + void SetPlanarReflectionsEnabled(bool enabled); + void TogglePlanarReflections(); + bool IsPlanarReflectionsEnabled() const { + return enablePlanarReflections; + } + + // Ray query rendering mode control + void SetRenderMode(RenderMode mode) { + currentRenderMode = mode; + } + RenderMode GetRenderMode() const { + return currentRenderMode; + } + void ToggleRenderMode() { + currentRenderMode = (currentRenderMode == RenderMode::Rasterization) ? RenderMode::RayQuery : RenderMode::Rasterization; + } + + // Ray query capability getters + bool GetRayQueryEnabled() const { + return rayQueryEnabled; + } + bool GetAccelerationStructureEnabled() const { + return accelerationStructureEnabled; + } + + // Ray Query static-only mode (disable animation/physics updates and TLAS refits to render a static opaque scene) + void SetRayQueryStaticOnly(bool v) { + rayQueryStaticOnly = v; + } + bool IsRayQueryStaticOnly() const { + return rayQueryStaticOnly; + } + + /** + * @brief Request acceleration structure build at next safe frame point. + * Safe to call from any thread (e.g., background loading thread). + */ + void RequestAccelerationStructureBuild() { + if (!accelerationStructureEnabled || !rayQueryEnabled) + return; + // Record when the request was made so the render loop can enforce a bounded deferral + // policy (avoid getting stuck waiting for “perfect” readiness forever). + // NOTE: `asBuildRequested` may already be true due to other triggers; still ensure + // the request timestamp is armed so the timeout logic can work. + if (asBuildRequestStartNs.load(std::memory_order_relaxed) == 0) { + const uint64_t nowNs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + asBuildRequestStartNs.store(nowNs, std::memory_order_relaxed); + } + // Allow AS build to take longer than the watchdog threshold (large scenes in Debug). + watchdogSuppressed.store(true, std::memory_order_relaxed); + asBuildRequested.store(true, std::memory_order_release); + } + // Overload with reason tracking for diagnostics + void RequestAccelerationStructureBuild(const char* reason) { + if (!accelerationStructureEnabled || !rayQueryEnabled) + return; + if (asBuildRequestStartNs.load(std::memory_order_relaxed) == 0) { + const uint64_t nowNs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + asBuildRequestStartNs.store(nowNs, std::memory_order_relaxed); + } + if (reason) { + lastASBuildRequestReason = reason; + std::cout << "[AS] Requesting rebuild. Reason: " << reason << std::endl; + } else { + lastASBuildRequestReason = "(no reason)"; + } + + // Explicit requests bypass the freeze to ensure dynamic objects (like balls) are added + asDevOverrideAllowRebuild = true; + + watchdogSuppressed.store(true, std::memory_order_relaxed); + asBuildRequested.store(true, std::memory_order_release); + } + + /** + * @brief Build acceleration structures for ray query rendering. + * @param entities The entities to include in the acceleration structures. + * @return True if successful, false otherwise. + */ + bool buildAccelerationStructures(const std::vector& entities); + + // Refit/UPDATE the TLAS with latest entity transforms (no rebuild) + bool refitTopLevelAS(const std::vector& entities, CameraComponent* camera); + + /** + * @brief Update ray query descriptor sets with current resources. + * @param frameIndex The frame index to update (or all frames if not specified). + * @return True if successful, false otherwise. + */ + bool updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vector& entities); + + /** + * @brief Create or resize light storage buffers to accommodate the given number of lights. + * @param lightCount The number of lights to accommodate. + * @return True if successful, false otherwise. + */ + bool createOrResizeLightStorageBuffers(size_t lightCount); + + /** + * @brief Update the light storage buffer with current light data. + * @param frameIndex The current frame index. + * @param lights The light data to upload. + * @return True if successful, false otherwise. + */ + bool updateLightStorageBuffer(uint32_t frameIndex, const std::vector& lights, CameraComponent* camera = nullptr); + + /** + * @brief Update all existing descriptor sets with new light storage buffer references. + * Called when light storage buffers are recreated to ensure descriptor sets reference valid buffers. + */ + // Update PBR descriptor sets to point to the latest light SSBOs. + // When allFrames=true, refresh all frames (use only when the device is idle — e.g., after waitIdle()). + // Otherwise, refresh only the current frame at the frame safe point to avoid touching in‑flight frames. + void updateAllDescriptorSetsWithNewLightBuffers(bool allFrames = false); + + // Upload helper: record both layout transitions and the copy in a single submit with a fence + void uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + const std::vector& regions, + uint32_t mipLevels = 1); + + // Generate full mip chain for a 2D color image using GPU blits + void generateMipmaps(vk::Image image, + vk::Format format, + int32_t texWidth, + int32_t texHeight, + uint32_t mipLevels); + + vk::Format findDepthFormat(); + + /** + * @brief Pre-allocate all Vulkan resources for an entity during scene loading. + * @param entity The entity to pre-allocate resources for. + * @return True if pre-allocation was successful, false otherwise. + */ + bool preAllocateEntityResources(Entity* entity); + + /** + * @brief Pre-allocate Vulkan resources for a batch of entities, batching mesh uploads. + * + * This variant is optimized for large scene loads (e.g., GLTF Bistro). It will: + * - Create per-mesh GPU buffers as usual, but record all buffer copy commands + * into a single command buffer and submit them in one batch. + * - Then create uniform buffers and descriptor sets per entity. + * + * Callers that load many geometry entities at once (like GLTF scene loading) + * should prefer this over repeated preAllocateEntityResources() calls. + */ + bool preAllocateEntityResourcesBatch(const std::vector& entities); + + // Thread-safe: enqueue entities that need GPU-side resource preallocation. + // The actual Vulkan work will be performed on the render thread at the frame-start safe point. + void EnqueueEntityPreallocationBatch(const std::vector& entities); + void EnqueueInstanceBufferRecreation(Entity* entity); + + /** + * @brief Recreate the instance buffer for an entity that had its instances cleared. + * + * When an entity that was originally set up for instanced rendering needs to be + * converted to a single non-instanced entity (e.g., for animation), this method + * recreates the GPU instance buffer with a single identity instance. + * + * @param entity The entity whose instance buffer should be recreated. + * @return True if successful, false otherwise. + */ + bool recreateInstanceBuffer(Entity* entity); + + // Shared default PBR texture identifiers (to avoid creating hundreds of identical textures) + static const std::string SHARED_DEFAULT_ALBEDO_ID; + static const std::string SHARED_DEFAULT_NORMAL_ID; + static const std::string SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + static const std::string SHARED_DEFAULT_OCCLUSION_ID; + static const std::string SHARED_DEFAULT_EMISSIVE_ID; + static const std::string SHARED_BRIGHT_RED_ID; + + /** + * @brief Determine the appropriate texture format based on the texture type. + * @param textureId The texture identifier to analyze. + * @return The appropriate Vulkan format (sRGB for baseColor, linear for others). + */ + static vk::Format determineTextureFormat(const std::string& textureId); + + public: + // OpenXR support + bool IsXrMode() const { return xrMode; } + XrContext& GetXrContext() { return xrContext; } + + private: + XrContext xrContext; + bool xrMode = false; + + // Platform + Platform* platform = nullptr; + + // Model loader reference for accessing extracted lights + class ModelLoader* modelLoader = nullptr; + + // PBR rendering parameters + float gamma = 2.2f; // Gamma correction value + float exposure = 1.2f; // HDR exposure value (default tuned to avoid washout) + float reflectionIntensity = 1.0f; // User control for glass reflection strength + // Raster shadows (experimental): use ray queries in the raster PBR fragment shader. + // Wired through `UniformBufferObject.padding2` to avoid UBO layout churn. + bool enableRasterRayQueryShadows = false; + + // Ray Query tuning + int rayQueryMaxBounces = 1; // 0 = no secondary rays, 1 = one-bounce reflection/refraction + bool enableRayQueryShadows = true; // Hard shadows for Ray Query direct lighting (shadow rays) + int rayQueryShadowSampleCount = 1; // 1 = hard; >1 enables soft-shadow sampling in the shader + float rayQueryShadowSoftness = 0.0f; // 0 = hard; otherwise scales effective light radius (fraction of range) + // Thick-glass controls (RQ-only) + bool enableThickGlass = true; + float thickGlassAbsorptionScale = 1.0f; + float thickGlassThicknessClamp = 0.2f; // meters + + // Vulkan RAII context + vk::raii::Context context; + + // Vulkan instance and debug messenger + vk::raii::Instance instance = nullptr; + vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; + + // Vulkan device + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + + // Memory pool for efficient memory management + std::unique_ptr memoryPool; + + // Vulkan queues + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::Queue computeQueue = nullptr; + + // Vulkan surface + vk::raii::SurfaceKHR surface = nullptr; + + // Swap chain + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent = {0, 0}; + std::vector swapChainImageViews; + // OpenXR Swapchains + std::vector eyeSwapchainImages[2]; + std::vector eyeSwapchainImageViews[2]; + + // Tracked layouts for swapchain images (VVL requires correct oldLayout in barriers). + // Initialized at swapchain creation and updated as we transition. + std::vector swapChainImageLayouts; + + // Dynamic rendering info + vk::RenderingInfo renderingInfo; + std::vector colorAttachments; + vk::RenderingAttachmentInfo depthAttachment; + + // Pipelines + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + vk::raii::PipelineLayout pbrPipelineLayout = nullptr; + vk::raii::Pipeline pbrGraphicsPipeline = nullptr; + vk::raii::Pipeline pbrBlendGraphicsPipeline = nullptr; + // Transparent PBR pipeline variant for premultiplied alpha content + vk::raii::Pipeline pbrPremulBlendGraphicsPipeline = nullptr; + // Opaque PBR pipeline variant used after a depth pre-pass (depth read-only, compare with pre-pass depth) + vk::raii::Pipeline pbrPrepassGraphicsPipeline = nullptr; + // Reflection PBR pipeline used for mirrored off-screen pass (cull none to avoid winding issues) + vk::raii::Pipeline pbrReflectionGraphicsPipeline = nullptr; + // Specialized pipeline for architectural glass (windows, lamp glass, etc.). + // Shares descriptor layouts and vertex input with the PBR pipelines but uses + // a dedicated fragment shader entry point for more stable glass shading. + vk::raii::Pipeline glassGraphicsPipeline = nullptr; + vk::raii::PipelineLayout lightingPipelineLayout = nullptr; + vk::raii::Pipeline lightingPipeline = nullptr; + + // Fullscreen composite pipeline to draw the opaque off-screen color to the swapchain + // (used to avoid gamma-incorrect vkCmdCopyImage and to apply tone mapping when desired). + vk::raii::PipelineLayout compositePipelineLayout = nullptr; + vk::raii::Pipeline compositePipeline = nullptr; + vk::raii::DescriptorSetLayout compositeDescriptorSetLayout = nullptr; // not used; reuse transparentDescriptorSetLayout + std::vector compositeDescriptorSets; // unused; reuse transparentDescriptorSets + + // Pipeline rendering create info structures (for proper lifetime management) + vk::PipelineRenderingCreateInfo mainPipelineRenderingCreateInfo; + vk::PipelineRenderingCreateInfo pbrPipelineRenderingCreateInfo; + vk::PipelineRenderingCreateInfo lightingPipelineRenderingCreateInfo; + vk::PipelineRenderingCreateInfo compositePipelineRenderingCreateInfo; + + // Create composite pipeline + bool createCompositePipeline(); + + // Compute pipeline + vk::raii::PipelineLayout computePipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; + vk::raii::DescriptorPool computeDescriptorPool = nullptr; + std::vector computeDescriptorSets; + vk::raii::CommandPool computeCommandPool = nullptr; + + // Thread safety for queue access - unified mutex since queues may share the same underlying VkQueue + mutable std::mutex queueMutex; + // Thread safety for descriptor pool/set operations across all engine threads + mutable std::mutex descriptorMutex; + // Monotonic generation counter for descriptor pool rebuilds (future use for hardening) + std::atomic descriptorPoolGeneration{0}; + + // Command pool and buffers + vk::raii::CommandPool commandPool = nullptr; + std::vector commandBuffers; + // Protect usage of shared commandPool for transient command buffers + mutable std::mutex commandMutex; + + // Dedicated transfer queue (falls back to graphics if unavailable) + vk::raii::Queue transferQueue = nullptr; + + // Synchronization objects + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + + // Upload timeline semaphore for transfer -> graphics handoff (signaled per upload) + vk::raii::Semaphore uploadsTimeline = nullptr; + // Tracks last timeline value that has been submitted for signaling on uploadsTimeline + std::atomic uploadTimelineLastSubmitted{0}; + + // Depth buffer + vk::raii::Image depthImage = nullptr; + std::unique_ptr depthImageAllocation = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + // Forward+ configuration + bool useForwardPlus = true; // default enabled + uint32_t forwardPlusTileSizeX = 16; + uint32_t forwardPlusTileSizeY = 16; + uint32_t forwardPlusSlicesZ = 16; // clustered depth slices + static constexpr uint32_t MAX_LIGHTS_PER_TILE = 256; // conservative cap + + struct TileHeader { + uint32_t offset; // into tileLightIndices + uint32_t count; // number of indices for this tile + uint32_t pad0; + uint32_t pad1; + }; + + struct ForwardPlusPerFrame { + // SSBOs for per-tile light lists + vk::raii::Buffer tileHeaders = nullptr; + std::unique_ptr tileHeadersAlloc = nullptr; + vk::raii::Buffer tileLightIndices = nullptr; + std::unique_ptr tileLightIndicesAlloc = nullptr; + size_t tilesCapacity = 0; // number of tiles allocated + size_t indicesCapacity = 0; // number of indices allocated + + // Uniform buffer with view/proj, screen size, tile size, etc. + vk::raii::Buffer params = nullptr; + std::unique_ptr paramsAlloc = nullptr; + void* paramsMapped = nullptr; + + // Optional compute debug output buffer (uints), host-visible + vk::raii::Buffer debugOut = nullptr; + std::unique_ptr debugOutAlloc = nullptr; + bool debugOutAwaitingReadback = false; + + // One-frame color probes (host-visible, small buffers) + vk::raii::Buffer probeOffscreen = nullptr; + std::unique_ptr probeOffscreenAlloc = nullptr; + vk::raii::Buffer probeSwapchain = nullptr; + std::unique_ptr probeSwapchainAlloc = nullptr; + bool probeAwaitingReadback = false; + + // Compute descriptor set for culling + vk::raii::DescriptorSet computeSet = nullptr; + }; + std::vector forwardPlusPerFrame; // size MAX_FRAMES_IN_FLIGHT + // Per-frame light count used by shaders (set once before main pass) + uint32_t lastFrameLightCount = 0; + + // Forward+ compute resources + vk::raii::PipelineLayout forwardPlusPipelineLayout = nullptr; + vk::raii::Pipeline forwardPlusPipeline = nullptr; + vk::raii::DescriptorSetLayout forwardPlusDescriptorSetLayout = nullptr; + + // Depth pre-pass pipeline + vk::raii::Pipeline depthPrepassPipeline = nullptr; + + // Ray query rendering mode + RenderMode currentRenderMode = RenderMode::RayQuery; + + // Ray query pipeline and resources + vk::raii::PipelineLayout rayQueryPipelineLayout = nullptr; + vk::raii::Pipeline rayQueryPipeline = nullptr; + vk::raii::DescriptorSetLayout rayQueryDescriptorSetLayout = nullptr; + std::vector rayQueryDescriptorSets; + // Track when the ray query descriptor set for each frame has been written. + // Updating binding 6 (large texture table) can be expensive; avoid doing it every frame. + std::vector rayQueryDescriptorsWritten; // size = MAX_FRAMES_IN_FLIGHT + // Bitmask of frames whose ray query descriptor set needs a refresh (e.g., after TLAS rebuild or texture upload). + std::atomic rayQueryDescriptorsDirtyMask{0}; + + // Dedicated ray query UBO (one per frame in flight) - separate from entity UBOs + std::vector rayQueryUniformBuffers; + std::vector> rayQueryUniformAllocations; + std::vector rayQueryUniformBuffersMapped; + + // Ray query output image (storage image for compute shader output) + vk::raii::Image rayQueryOutputImage = nullptr; + std::unique_ptr rayQueryOutputImageAllocation = nullptr; + vk::raii::ImageView rayQueryOutputImageView = nullptr; + + // Acceleration structures for ray query + struct AccelerationStructure { + vk::raii::Buffer buffer = nullptr; + std::unique_ptr allocation = nullptr; + vk::raii::AccelerationStructureKHR handle = nullptr; // Use RAII for proper lifetime management + vk::DeviceAddress deviceAddress = 0; + }; + std::vector blasStructures; // Bottom-level AS (one per mesh) + AccelerationStructure tlasStructure; // Top-level AS (scene) + + // Deferred deletion queue for old AS structures + // Keeps old AS buffers alive until all frames in flight have finished using them + struct PendingASDelete { + std::vector blasStructures; + AccelerationStructure tlasStructure; + uint32_t framesSinceDestroy = 0; // Increment each frame, delete when >= MAX_FRAMES_IN_FLIGHT + }; + std::vector pendingASDeletions; + + // GPU data structures for ray query proper normal and material access + struct GeometryInfo { + uint64_t vertexBufferAddress; // Device address of vertex buffer + uint64_t indexBufferAddress; // Device address of index buffer + uint32_t vertexCount; // Number of vertices + uint32_t materialIndex; // Index into material buffer + uint32_t indexCount; // Number of indices (to bound primitiveIndex in shader) + uint32_t _pad0; + // Instance-space -> world-space normal transform (3 columns). Matches raster convention. + // Stored as float4 columns (xyz used, w unused) for stable std430 layout. + alignas(16) glm::vec4 normalMatrix0; + alignas(16) glm::vec4 normalMatrix1; + alignas(16) glm::vec4 normalMatrix2; + }; + + struct MaterialData { + alignas(16) glm::vec3 albedo; + alignas(4) float metallic; + alignas(16) glm::vec3 emissive; + alignas(4) float roughness; + alignas(4) float ao; + alignas(4) float ior; + alignas(4) float emissiveStrength; + alignas(4) float alpha; + alignas(4) float transmissionFactor; + alignas(4) float alphaCutoff; + // glTF alpha mode encoding (matches shader): 0=OPAQUE, 1=MASK, 2=BLEND + alignas(4) int32_t alphaMode; + alignas(4) uint32_t isGlass; // bool as uint32 + alignas(4) uint32_t isLiquid; // bool as uint32 + + // Thick-glass parameters (RQ-only) + alignas(16) glm::vec3 absorptionColor{1.0f, 1.0f, 1.0f}; + alignas(4) float absorptionDistance = 1.0f; // meters + alignas(4) uint32_t thinWalled = 1u; // 1 = thin surface, 0 = thick volume + + // Raster parity: texture-set flags (-1 = no texture; 0 = sample from binding 6 table). + // Ray Query uses a single texture table (binding 6); indices are always valid even when + // the set flag is -1, so the shader can choose the correct no-texture behavior. + alignas(4) int32_t baseColorTextureSet; + alignas(4) int32_t physicalDescriptorTextureSet; + alignas(4) int32_t normalTextureSet; + alignas(4) int32_t occlusionTextureSet; + alignas(4) int32_t emissiveTextureSet; + + // Ray Query texture table indices (binding 6). These always reference a valid descriptor + // (real streamed texture or a shared default slot). + alignas(4) int32_t baseColorTexIndex; + alignas(4) int32_t normalTexIndex; + alignas(4) int32_t physicalTexIndex; // metallic-roughness (default) or spec-gloss when useSpecGlossWorkflow=1 + alignas(4) int32_t occlusionTexIndex; + alignas(4) int32_t emissiveTexIndex; + + // Specular-glossiness workflow support (KHR_materials_pbrSpecularGlossiness) + alignas(4) int32_t useSpecGlossWorkflow; // 1 if SpecGloss + alignas(4) float glossinessFactor; + alignas(16) glm::vec3 specularFactor; + alignas(4) int32_t hasEmissiveStrengthExt; + alignas(4) uint32_t _padMat[3]; + }; + + // Ray query geometry and material buffers + vk::raii::Buffer geometryInfoBuffer = nullptr; + std::unique_ptr geometryInfoAllocation = nullptr; + vk::raii::Buffer materialBuffer = nullptr; + std::unique_ptr materialAllocation = nullptr; + + // Ray query baseColor texture array (binding 6) + static constexpr uint32_t RQ_MAX_TEX = 2048; + // Reserved slots in the Ray Query texture table (binding 6) + static constexpr uint32_t RQ_SLOT_DEFAULT_BASECOLOR = 0; + static constexpr uint32_t RQ_SLOT_DEFAULT_NORMAL = 1; + static constexpr uint32_t RQ_SLOT_DEFAULT_METALROUGH = 2; + static constexpr uint32_t RQ_SLOT_DEFAULT_OCCLUSION = 3; + static constexpr uint32_t RQ_SLOT_DEFAULT_EMISSIVE = 4; + // NOTE: Textures can stream in asynchronously and their underlying VkImageView/VkSampler + // can be destroyed/recreated. Therefore, the Ray Query texture table must NOT cache + // VkDescriptorImageInfo (which contains raw handles). Instead, cache only the canonical + // texture key per slot and rebuild VkDescriptorImageInfo each descriptor update. + // + // Slots 0..4 are reserved for shared default PBR textures. + std::vector rayQueryTexKeys; // slot -> canonical texture key + std::vector rayQueryTexFallbackSlots; // slot -> fallback slot (type-appropriate default) + uint32_t rayQueryTexCount = 0; // number of valid slots in rayQueryTexKeys + std::unordered_map rayQueryTexIndex; // canonicalKey -> slot + + // Per-material texture path mapping captured at AS build time; used for streaming requests + // and debugging, but Ray Query primarily uses per-material texture indices. + struct RQMaterialTexPaths { + std::string baseColor; + std::string normal; + std::string physical; + std::string occlusion; + std::string emissive; + }; + std::vector rqMaterialTexPaths; + + // Count of GeometryInfo instances currently uploaded (CPU-side tracking) + size_t geometryInfoCountCPU = 0; + // Count of materials currently uploaded (CPU-side tracking) + size_t materialCountCPU = 0; + + // --- Pending GPU uploads (to be executed on the render thread safe point) --- + std::mutex pendingMeshUploadsMutex; + std::vector pendingMeshUploads; // meshes with staged data to copy + + struct InFlightMeshUploadBatch { + uint64_t signalValue = 0; + std::vector meshes; + std::unique_ptr commandPool; + std::unique_ptr commandBuffers; + }; + std::mutex inFlightMeshUploadsMutex; + std::deque inFlightMeshUploads; + + // Enqueue mesh uploads collected on background/loading threads + void EnqueueMeshUploads(const std::vector& meshes); + // Execute pending mesh uploads on the render thread (called from Render after fence wait) + void ProcessPendingMeshUploads(); + + // --- Pending entity GPU preallocation (enqueued by scene loader thread; executed on render thread) --- + std::mutex pendingEntityPreallocMutex; + std::vector pendingEntityPrealloc; + std::vector pendingInstanceBufferRecreations; + std::atomic pendingEntityPreallocQueued{false}; + void ProcessPendingEntityPreallocations(); + + // Descriptor set layouts (declared before pools and sets) + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout transparentDescriptorSetLayout = nullptr; + vk::raii::PipelineLayout pbrTransparentPipelineLayout = nullptr; + + // The texture that will hold a snapshot of the opaque scene + // One off-screen color image per frame-in-flight to avoid cross-frame read/write hazards. + std::vector opaqueSceneColorImages; + std::vector> opaqueSceneColorImageAllocations; + std::vector opaqueSceneColorImageViews; + // Track the current layout per frame (initialized to eUndefined at creation) + std::vector opaqueSceneColorImageLayouts; + vk::raii::Sampler opaqueSceneColorSampler{nullptr}; + + // A descriptor set for the opaque scene color texture. One per frame in flight. + std::vector transparentDescriptorSets; + // Fallback descriptor sets for opaque pass (binds a default SHADER_READ_ONLY texture as Set 1) + std::vector transparentFallbackDescriptorSets; + + // Ray Query composite descriptor sets: sample the rayQueryOutputImage in a fullscreen pass + std::vector rqCompositeDescriptorSets; + // Fallback sampler for the RQ composite if no other sampler is available at init time + vk::raii::Sampler rqCompositeSampler{nullptr}; + + // Mesh resources + struct MeshResources { + // Device-local vertex/index buffers used for rendering + vk::raii::Buffer vertexBuffer = nullptr; + std::unique_ptr vertexBufferAllocation = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + std::unique_ptr indexBufferAllocation = nullptr; + uint32_t indexCount = 0; + + // Optional per-mesh staging buffers used when uploads are batched. + // These are populated when createMeshResources(..., deferUpload=true) is used + // and are consumed and cleared by preAllocateEntityResourcesBatch(). + vk::raii::Buffer stagingVertexBuffer = nullptr; + vk::raii::DeviceMemory stagingVertexBufferMemory = nullptr; + vk::DeviceSize vertexBufferSizeBytes = 0; + + vk::raii::Buffer stagingIndexBuffer = nullptr; + vk::raii::DeviceMemory stagingIndexBufferMemory = nullptr; + vk::DeviceSize indexBufferSizeBytes = 0; + + // Material index for ray query (extracted from entity name or MaterialMesh) + int32_t materialIndex = -1; // -1 = no material/default + }; + std::unordered_map meshResources; + + // Texture resources + struct TextureResources { + vk::raii::Image textureImage = nullptr; + std::unique_ptr textureImageAllocation = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + vk::Format format = vk::Format::eR8G8B8A8Srgb; // Store texture format for proper color space handling + uint32_t mipLevels = 1; // Store number of mipmap levels + // Hint: true if source texture appears to use alpha masking (any alpha < ~1.0) + bool alphaMaskedHint = false; + }; + std::unordered_map textureResources; + + // Pending texture jobs that require GPU-side work. Worker threads + // enqueue these jobs; the main thread drains them and performs the + // actual LoadTexture/LoadTextureFromMemory calls. + struct PendingTextureJob { + enum class Type { + FromFile, + FromMemory + } type; + enum class Priority { + Critical, + NonCritical + } priority; + std::string idOrPath; + std::vector data; // only used for FromMemory + int width = 0; + int height = 0; + int channels = 0; + }; + + std::mutex pendingTextureJobsMutex; + std::condition_variable pendingTextureCv; + std::vector pendingTextureJobs; + // Track outstanding critical texture jobs (for IsLoading) + std::atomic criticalJobsOutstanding{0}; + + // Background uploader worker controls (multiple workers) + std::atomic stopUploadsWorker{false}; + std::vector uploadsWorkerThreads; + + // Track how many texture upload jobs have been scheduled vs completed + // on the GPU side. Used only for UI feedback during streaming. + std::atomic uploadJobsTotal{0}; + std::atomic uploadJobsCompleted{0}; + // When true, initial scene load is complete and the loading overlay should be hidden + std::atomic initialLoadComplete{false}; + // Loading-phase UI state (atomic because ImGui may query at any point) + std::atomic loadingPhase{static_cast(LoadingPhase::Scene)}; + std::atomic loadingPhaseProgress{0.0f}; + + // Performance counters for texture uploads + std::atomic bytesUploadedTotal{0}; + // Streaming window start time in nanoseconds from steady_clock epoch (0 when inactive) + std::atomic uploadWindowStartNs{0}; + // Aggregate per-texture CPU upload durations (nanoseconds) and count + std::atomic totalUploadNs{0}; + std::atomic uploadCount{0}; + + // Reverse mapping from texture ID to entities that reference it. Used to + // update descriptor sets when a streamed texture finishes uploading. + std::mutex textureUsersMutex; + std::unordered_map> textureToEntities; + + // Entities needing descriptor set refresh due to streamed textures + std::mutex dirtyEntitiesMutex; + // Map of entity -> bitmask of frames-in-flight that still need a descriptor refresh. + // This avoids the “frame 0 updated / frame 1 still default” oscillation when + // MAX_FRAMES_IN_FLIGHT > 1 and a texture becomes available mid-stream. + std::unordered_map descriptorDirtyEntities; + + // Protect concurrent access to textureResources + mutable std::shared_mutex textureResourcesMutex; + + // Texture aliasing: maps alias (canonical) IDs to actual loaded keys + std::unordered_map textureAliases; + + // Per-texture load de-duplication (serialize loads of the same texture ID only) + mutable std::mutex textureLoadStateMutex; + std::condition_variable textureLoadStateCv; + std::unordered_set texturesLoading; + + // Serialize GPU-side texture upload (image/buffer creation, transitions) to avoid driver/memory pool races + mutable std::mutex textureUploadMutex; + + // Thread pool for background background tasks (textures, etc.) + std::unique_ptr threadPool; + // Mutex to protect threadPool access during initialization/cleanup + mutable std::shared_mutex threadPoolMutex; + + // Texture loading progress (for UI) + std::atomic textureTasksScheduled{0}; + std::atomic textureTasksCompleted{0}; + std::atomic loadingFlag{false}; + + // Acceleration structure build UI progress (written on render thread). + // Kept as atomics because ImGui can query at any point during the frame. + std::atomic asBuildUiActive{false}; + std::atomic asBuildUiProgress{0.0f}; + std::atomic asBuildUiDone{0}; + std::atomic asBuildUiTotal{0}; + std::atomic asBuildUiStage{"idle"}; + std::atomic asBuildUiStartNs{0}; + + // Default texture resources (used when no texture is provided) + TextureResources defaultTextureResources; + + // Performance clamps (to reduce per-frame cost) + static constexpr uint32_t MAX_ACTIVE_LIGHTS = 1024; // Limit the number of lights processed per frame + + // Static lights loaded during model initialization + std::vector staticLights; + + // Dynamic lighting system using storage buffers + struct LightStorageBuffer { + vk::raii::Buffer buffer = nullptr; + std::unique_ptr allocation = nullptr; + void* mapped = nullptr; + size_t capacity = 0; // Current capacity in number of lights + size_t size = 0; // Current number of lights + }; + std::vector lightStorageBuffers; // One per frame in flight + + // Entity resources (contains descriptor sets - must be declared before descriptor pool) + struct EntityResources { + std::vector uniformBuffers; + std::vector> uniformBufferAllocations; + std::vector uniformBuffersMapped; + std::vector basicDescriptorSets; // For basic pipeline + std::vector pbrDescriptorSets; // For PBR pipeline + + // Instance buffer for instanced rendering + vk::raii::Buffer instanceBuffer = nullptr; + std::unique_ptr instanceBufferAllocation = nullptr; + void* instanceBufferMapped = nullptr; + + // Tracks whether binding 0 (UBO) has been written at least once for each frame + // for each pipeline type. Descriptor sets for non-current frames are allocated + // but not necessarily initialized immediately (to avoid update-after-bind hazards), + // so each frame needs a one-time initialization at its safe point. + std::vector pbrUboBindingWritten; // size = MAX_FRAMES_IN_FLIGHT + std::vector basicUboBindingWritten; // size = MAX_FRAMES_IN_FLIGHT + + // Tracks whether image bindings have been written at least once for each frame. + // If false for the current frame at the safe point, we cold-initialize the + // image bindings (PBR: b1..b5 [+b6 when applicable], Basic: b1) with either + // real textures or shared defaults to avoid per-frame "black" flashes. + std::vector pbrImagesWritten; // size = MAX_FRAMES_IN_FLIGHT + std::vector basicImagesWritten; // size = MAX_FRAMES_IN_FLIGHT + + // Tracks whether the remaining required bindings in the PBR set 0 layout have + // been written at least once for each frame. + // This includes bindings like Forward+ tile buffers (7/8), reflection sampler (10), + // and TLAS (11). These bindings are required by the pipeline layout and must be + // valid before any draw that uses the PBR/glass pipelines. + std::vector pbrFixedBindingsWritten; // size = MAX_FRAMES_IN_FLIGHT + + // Cached material lookup/classification for raster rendering. + // Avoids per-frame string parsing of entity names ("_Material_") and repeated + // ModelLoader material lookups across culling, sorting, and draw loops. + bool materialCacheValid = false; + const Material* cachedMaterial = nullptr; + // Derived flags used by render queues and sorting heuristics + bool cachedIsBlended = false; + bool cachedIsGlass = false; + bool cachedIsLiquid = false; + // Material-derived push constants defaults (static per-entity unless material changes) + MaterialProperties cachedMaterialProps{}; + }; + + // Cached job for rendering a single entity in a frame + struct RenderJob + { + Entity *entity; + EntityResources *entityRes; + MeshResources *meshRes; + MeshComponent *meshComp; + TransformComponent *transformComp; + bool isAlphaMasked; + }; + std::unordered_map entityResources; + + // Descriptor pool (declared after entity resources to ensure proper destruction order) + vk::raii::DescriptorPool descriptorPool = nullptr; + + // Current frame index + uint32_t currentFrame = 0; + + // Queue family indices + QueueFamilyIndices queueFamilyIndices; + + // Validation layers + const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" + }; + + // Required device extensions + const std::vector requiredDeviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME + }; + + // Optional device extensions + const std::vector optionalDeviceExtensions = { + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, + VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, + // Robustness and safety + VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, + // Tile/local memory friendly dynamic rendering readback + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + // Shader tile image for fast tile access + VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME, + // Ray query support for ray-traced rendering + VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME, + VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, + VK_KHR_RAY_QUERY_EXTENSION_NAME + }; + + // All device extensions (required + optional) + std::vector deviceExtensions; + + // Initialization flag + bool initialized = false; + // Whether VK_EXT_descriptor_indexing (update-after-bind) path is enabled + bool descriptorIndexingEnabled = false; + bool storageAfterBindEnabled = false; + // Feature toggles detected/enabled at device creation + bool robustness2Enabled = false; + bool dynamicRenderingLocalReadEnabled = false; + bool shaderTileImageEnabled = false; + bool rayQueryEnabled = false; + bool accelerationStructureEnabled = false; + + // When true and current render mode is RayQuery, the engine renders a static opaque scene: + // - Animation/physics updates are suppressed by the Engine (input/Update hook) + // - TLAS refit per-frame is skipped to avoid any animation-driven changes + // - The AS is built once after loading completes + // Default now OFF so animation is enabled again for AS (per user request) + bool rayQueryStaticOnly = false; + + // (No debug-only TLAS filtering in production.) + + // Framebuffer resized flag (atomic to handle platform callback vs. render thread) + std::atomic framebufferResized{false}; + // Guard to prevent descriptor updates while a command buffer is recording + std::atomic isRecordingCmd{false}; + // Descriptor sets may be temporarily invalid during swapchain recreation; suppress updates then. + std::atomic descriptorSetsValid{true}; + // Request flag for acceleration structure build (set by loading thread, cleared by render thread) + std::atomic asBuildRequested{false}; + // Timestamp of the most recent AS build request (steady_clock ns). Used to prevent infinite deferral. + std::atomic asBuildRequestStartNs{0}; + + // Track last successfully built AS sizes to avoid rebuilding with a smaller subset + // (e.g., during incremental streaming where not all meshes are ready yet). + // We only accept AS builds that are monotonically non-decreasing in counts. + size_t lastASBuiltBLASCount = 0; + // NOTE: This is the number of renderable ENTITIES included in the AS build (not TLAS instances). + size_t lastASBuiltInstanceCount = 0; + // TLAS instance count (includes per-mesh instancing). Used for logging and shader bounds. + size_t lastASBuiltTlasInstanceCount = 0; + + // Freeze TLAS rebuilds after a full build to prevent regressions (e.g., animation-only TLAS) + bool asFreezeAfterFullBuild = true; // enable freezing behavior + bool asFrozen = false; // once frozen, ignore rebuilds unless explicitly overridden + // Optional developer override to allow rebuild while frozen + bool asDevOverrideAllowRebuild = false; + // Reason string for the last time a build was requested (for logging) + std::string lastASBuildRequestReason; + + // Opportunistic rebuilds (when counts increase) can cause unintended TLAS churn during animation. + // Leave this disabled by default; TLAS builds should be explicit (on mode switch / scene ready). + bool asOpportunisticRebuildEnabled = false; + + // --- AS UPDATE/Refit state --- + // Persistent TLAS instances buffer & order for UPDATE (refit) + struct TlasInstanceRef { + class Entity* entity{nullptr}; + uint32_t instanceIndex{0}; // valid only when instanced==true + bool instanced{false}; // true when this TLAS entry comes from MeshComponent instancing + }; + vk::raii::Buffer tlasInstancesBuffer{nullptr}; + std::unique_ptr tlasInstancesAllocation; + uint32_t tlasInstanceCount = 0; + std::vector tlasInstanceOrder; // order must match buffer instances + + // Scratch buffer for TLAS UPDATE operations + vk::raii::Buffer tlasUpdateScratchBuffer{nullptr}; + std::unique_ptr tlasUpdateScratchAllocation; + + // Maximum number of frames in flight + // More than 1 allows CPU/GPU overlap and reduce per-frame stalls. + // All per-frame resources (UBOs, descriptor sets, reflection RTs, etc.) + // are sized dynamically based on this value. + const uint32_t MAX_FRAMES_IN_FLIGHT = 2u; + + // --- Performance & diagnostics --- + UniformBufferObject frameUboTemplate{}; + bool enableFrustumCulling = true; + uint32_t lastCullingVisibleCount = 0; + uint32_t lastCullingCulledCount = 0; + // Distance-based LOD (projected-size skip in pixels) + bool enableDistanceLOD = true; + float lodPixelThresholdOpaque = 1.5f; + float lodPixelThresholdTransparent = 2.5f; + // Sampler anisotropy preference (clamped to device limits) + float samplerMaxAnisotropy = 8.0f; + // Upper bound on auto-generated mip levels (to avoid excessive VRAM use on huge textures) + uint32_t maxAutoGeneratedMipLevels = 4; + + // --- Planar reflections (scaffolding) --- + bool enablePlanarReflections = false; // UI toggle to enable/disable planar reflections + float reflectionResolutionScale = 0.5f; // Scale relative to swapchain size + // Cached per-frame reflection data used by UBO population + // Current frame's reflection VP (for rendering the reflection pass) + glm::mat4 currentReflectionVP{1.0f}; + glm::vec4 currentReflectionPlane{0.0f, 1.0f, 0.0f, 0.0f}; + // Per-frame stored reflection VP (written during reflection pass) + std::vector reflectionVPs; // size MAX_FRAMES_IN_FLIGHT + // The VP to sample in the main pass (prev-frame VP to match prev-frame texture) + glm::mat4 sampleReflectionVP{1.0f}; + bool reflectionResourcesDirty = false; // recreate reflection RTs at safe point + + // --- Ray query rendering options --- + bool enableRayQueryReflections = true; // UI toggle to enable reflections in ray query mode + bool enableRayQueryTransparency = true; // UI toggle to enable transparency/refraction in ray query mode + + // === Watchdog system to detect application hangs === + // Atomic timestamp updated every frame - watchdog thread checks if stale + std::atomic lastFrameUpdateTime; + // Low-noise progress marker to pinpoint where the render thread stalled when the watchdog fires + std::atomic watchdogProgressLabel{"init"}; + // Optional numeric marker to help pinpoint stalls inside large loops + std::atomic watchdogProgressIndex{0}; + std::thread watchdogThread; + std::atomic watchdogRunning{false}; + // Some operations (notably BLAS/TLAS builds in Debug on large scenes) can legitimately take + // longer than the watchdog threshold. When set, the watchdog will not abort. + std::atomic watchdogSuppressed{false}; + + // === Descriptor update deferral while recording === + struct PendingDescOp { + Entity* entity; + std::string texPath; + bool usePBR; + uint32_t frameIndex; + bool imagesOnly; + }; + std::mutex pendingDescMutex; + std::vector pendingDescOps; // flushed at frame safe point + std::atomic descriptorRefreshPending{false}; + + struct ReflectionRT { + vk::raii::Image color{nullptr}; + std::unique_ptr colorAlloc{nullptr}; + vk::raii::ImageView colorView{nullptr}; + vk::raii::Sampler colorSampler{nullptr}; + + vk::raii::Image depth{nullptr}; + std::unique_ptr depthAlloc{nullptr}; + vk::raii::ImageView depthView{nullptr}; + + uint32_t width{0}; + uint32_t height{0}; + }; + std::vector reflections; // one per frame-in-flight + + // Private methods + bool createInstance(const std::string& appName, bool enableValidationLayers); + bool setupDebugMessenger(bool enableValidationLayers); + bool createSurface(); + bool checkValidationLayerSupport() const; + bool pickPhysicalDevice(); + void addSupportedOptionalExtensions(); + bool createLogicalDevice(bool enableValidationLayers); + bool createSwapChain(); + bool createImageViews(); + bool setupDynamicRendering(); + bool createDescriptorSetLayout(); + bool createPBRDescriptorSetLayout(); + bool createGraphicsPipeline(); + + bool createPBRPipeline(); + bool createLightingPipeline(); + bool createDepthPrepassPipeline(); + bool createForwardPlusPipelinesAndResources(); + + // Ray query pipeline creation + bool createRayQueryDescriptorSetLayout(); + bool createRayQueryPipeline(); + bool createRayQueryResources(); + // If updateOnlyCurrentFrame is true, only descriptor sets for currentFrame will be updated. + // Use updateOnlyCurrentFrame=false during initialization/swapchain recreation when the device is idle. + bool createOrResizeForwardPlusBuffers(uint32_t tilesX, uint32_t tilesY, uint32_t slicesZ, bool updateOnlyCurrentFrame = false); + void updateForwardPlusParams(uint32_t frameIndex, const glm::mat4& view, const glm::mat4& proj, uint32_t lightCount, uint32_t tilesX, uint32_t tilesY, uint32_t slicesZ, float nearZ, float farZ); + void dispatchForwardPlus(vk::raii::CommandBuffer& cmd, uint32_t tilesX, uint32_t tilesY, uint32_t slicesZ); + // Ensure Forward+ compute descriptor set binding 0 (lights SSBO) is bound for a frame + void refreshForwardPlusComputeLightsBindingForFrame(uint32_t frameIndex); + bool createComputePipeline(); + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) const; + bool createCommandPool(); + + // Shadow mapping methods + bool createComputeCommandPool(); + bool createDepthResources(); + bool createTextureImage(const std::string& texturePath, TextureResources& resources); + bool createTextureImageView(TextureResources& resources); + bool createTextureSampler(TextureResources& resources); + bool createDefaultTextureResources(); + bool createSharedDefaultPBRTextures(); + bool createMeshResources(MeshComponent* meshComponent, bool deferUpload = false); + bool createUniformBuffers(Entity* entity); + bool createDescriptorPool(); + bool createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR = false); + bool createDescriptorSets(Entity *entity, EntityResources &res, const std::string &texturePath, bool usePBR = false); + bool updateDescriptorSetsForFrame(Entity *entity, + const std::string &texturePath, + bool usePBR, + uint32_t frameIndex, + bool imagesOnly = false, + bool uboOnly = false); + bool updateDescriptorSetsForFrame(Entity *entity, + EntityResources &res, + const std::string &texturePath, + bool usePBR, + uint32_t frameIndex, + bool imagesOnly = false, + bool uboOnly = false); + // Refresh only the currentFrame PBR descriptor set bindings that Forward+ relies on + // (b6 = lights SSBO, b7 = tile headers, b8 = tile indices). Safe to call after + // we've waited on the frame fence at the start of Render(). + void refreshPBRForwardPlusBindingsForFrame(uint32_t frameIndex); + bool createCommandBuffers(); + bool createSyncObjects(); + + void cleanupSwapChain(); + + // Planar reflection helpers (initial scaffolding) + bool createReflectionResources(uint32_t width, uint32_t height); + void destroyReflectionResources(); + // Render the scene into the reflection RT (mirrored about a plane) — to be fleshed out next step + void renderReflectionPass(vk::raii::CommandBuffer& cmd, + const glm::vec4& planeWS, + CameraComponent* camera, + const std::vector &jobs); + + // Ensure Vulkan-Hpp dispatcher is initialized for the current thread when using RAII objects on worker threads + void ensureThreadLocalVulkanInit() const; + + // Cache and classify an entity's material for raster rendering (opaque vs blended, glass/liquid flags, + // and push-constant defaults). This avoids repeated per-frame string parsing and material lookups. + void ensureEntityMaterialCache(Entity* entity, EntityResources &res); + + // ===================== Culling helpers ===================== + struct FrustumPlanes { + // Plane equation ax + by + cz + d >= 0 considered inside + glm::vec4 planes[6]{}; // 0=L,1=R,2=B,3=T,4=N,5=F + }; + + static FrustumPlanes extractFrustumPlanes(const glm::mat4& vp); + + static void transformAABB(const glm::mat4& M, + const glm::vec3& localMin, + const glm::vec3& localMax, + glm::vec3& outMin, + glm::vec3& outMax); + + static bool aabbIntersectsFrustum(const glm::vec3& worldMin, + const glm::vec3& worldMax, + const FrustumPlanes& frustum); + void recreateSwapChain(); + + void updateUniformBuffer(uint32_t currentImage, Entity* entity, EntityResources *entityRes, CameraComponent* camera, TransformComponent *tc = nullptr); + void updateUniformBuffer(uint32_t currentImage, Entity* entity, EntityResources *entityRes, CameraComponent* camera, const glm::mat4& customTransform); + void updateUniformBuffer(uint32_t currentFrame, uint32_t eye, const glm::mat4& view, const glm::mat4& proj, const glm::vec3& camPos); + void updateUniformBufferInternal(uint32_t currentImage, Entity* entity, EntityResources *entityRes, CameraComponent* camera, UniformBufferObject& ubo); + void prepareFrameUboTemplate(CameraComponent *camera); + void drawRenderJob(const vk::raii::CommandBuffer& cmd, const RenderJob& job, uint32_t currentFrame, uint32_t eye, bool transparent); + + vk::raii::ShaderModule createShaderModule(const std::vector& code); + + QueueFamilyIndices findQueueFamilies(const vk::raii::PhysicalDevice& device); + SwapChainSupportDetails querySwapChainSupport(const vk::raii::PhysicalDevice& device); + bool isDeviceSuitable(vk::raii::PhysicalDevice& device); + bool checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device); + + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats); + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes); + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); + + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + + std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); + bool createOpaqueSceneColorResources(); + void createTransparentDescriptorSets(); + void createTransparentFallbackDescriptorSets(); + std::pair> createBufferPooled(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); + void copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size); + + std::pair createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t arrayLayers = 1); + std::pair> createImagePooled(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t mipLevels = 1, uint32_t arrayLayers = 1, vk::SharingMode sharingMode = vk::SharingMode::eExclusive, const std::vector& queueFamilies = {}); + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1, uint32_t layerCount = 1); + void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, vk::ArrayProxy regions); + // Extended: track stagedBytes for perf stats + void uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + vk::ArrayProxy regions, + uint32_t mipLevels, + vk::DeviceSize stagedBytes); + + vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 1, uint32_t layerCount = 1); + vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); + bool hasStencilComponent(vk::Format format); + + std::vector readFile(const std::string& filename); + + // Background uploader helpers + void StartUploadsWorker(size_t workerCount = 0); + void StopUploadsWorker(); + + // Serialize descriptor writes vs command buffer recording to avoid mid-record updates during recording + std::mutex renderRecordMutex; + + // (Descriptor API wrappers were considered but avoided here to keep RAII types intact.) + + // Upload perf getters + public: + uint64_t GetBytesUploadedTotal() const { + return bytesUploadedTotal.load(std::memory_order_relaxed); + } + double GetAverageUploadMs() const { + uint64_t ns = totalUploadNs.load(std::memory_order_relaxed); + uint32_t cnt = uploadCount.load(std::memory_order_relaxed); + if (cnt == 0) + return 0.0; + return static_cast(ns) / 1e6 / static_cast(cnt); + } + double GetUploadThroughputMBps() const { + uint64_t startNs = uploadWindowStartNs.load(std::memory_order_relaxed); + if (startNs == 0) + return 0.0; + auto now = std::chrono::steady_clock::now().time_since_epoch(); + uint64_t nowNs = static_cast(std::chrono::duration_cast(now).count()); + if (nowNs <= startNs) + return 0.0; + double seconds = static_cast(nowNs - startNs) / 1e9; + double mb = static_cast(bytesUploadedTotal.load(std::memory_order_relaxed)) / (1024.0 * 1024.0); + return seconds > 0.0 ? (mb / seconds) : 0.0; + } +}; diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp new file mode 100644 index 000000000..dc233602c --- /dev/null +++ b/attachments/openxr_engine/renderer_core.cpp @@ -0,0 +1,1194 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "renderer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file + +#include +#include // For PFN_vkGetInstanceProcAddr and C types +#include + +// Debug callback for vk::raii - uses raw Vulkan C types for cross-platform compatibility +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + [[maybe_unused]] void* pUserData) { + if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Print a message to the console + std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + } else { + // Print a message to the console + std::cout << "Validation layer: " << pCallbackData->pMessage << std::endl; + } + + return VK_FALSE; +} + +// Vulkan-Hpp style callback signature for newer headers expecting vk:: types +static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallbackVkHpp( + vk::DebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + [[maybe_unused]] vk::DebugUtilsMessageTypeFlagsEXT messageType, + const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, + [[maybe_unused]] void* pUserData) { + if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { + std::cerr << "Validation layer: " << pCallbackData->pMessage << std::endl; + } else { + std::cout << "Validation layer: " << pCallbackData->pMessage << std::endl; + } + return vk::False; +} + +// Watchdog thread function - monitors frame updates and aborts if application hangs +static void WatchdogThreadFunc(std::atomic* lastFrameTime, + std::atomic* running, + std::atomic* suppressed, + std::atomic* progressLabel, + std::atomic* progressIndex) { + while (running->load(std::memory_order_relaxed)) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + + if (!running->load(std::memory_order_relaxed)) { + break; // Shutdown requested + } + + // Check if frame timestamp was updated recently. + // Some operations (e.g., BLAS/TLAS builds in Debug on large scenes) can legitimately take + // much longer than 5 or 10 seconds. When suppressed, allow a longer grace period. + auto now = std::chrono::steady_clock::now(); + auto lastUpdate = lastFrameTime->load(std::memory_order_relaxed); + auto elapsed = std::chrono::duration_cast(now - lastUpdate).count(); + const int64_t allowedSeconds = (suppressed && suppressed->load(std::memory_order_relaxed)) ? 60 : 10; + + if (elapsed >= allowedSeconds) { + // APPLICATION HAS HUNG - no frame updates for 10+ seconds + const char* label = nullptr; + if (progressLabel) { + label = progressLabel->load(std::memory_order_relaxed); + } + uint32_t idx = 0; + if (progressIndex) { + idx = progressIndex->load(std::memory_order_relaxed); + } + + std::cerr << "\n\n"; + std::cerr << "========================================\n"; + std::cerr << "WATCHDOG: APPLICATION HAS HUNG!\n"; + std::cerr << "========================================\n"; + std::cerr << "Last frame update was " << elapsed << " seconds ago.\n"; + if (label && label[0] != '\0') { + std::cerr << "Last progress marker: " << label << "\n"; + } + if (progressIndex) { + std::cerr << "Progress index: " << idx << "\n"; + } + std::cerr << "The render loop is not progressing.\n"; + std::cerr << "Aborting to generate stack trace...\n"; + std::cerr << "========================================\n\n"; + std::abort(); // Force crash with stack trace + } + } + + std::cout << "[Watchdog] Stopped\n"; +} + +// Renderer core implementation for the "Rendering Pipeline" chapter of the tutorial. +Renderer::Renderer(Platform* platform) : platform(platform) { + // Initialize deviceExtensions with required extensions only + // Optional extensions will be added later after checking device support + deviceExtensions = requiredDeviceExtensions; +} + +// Destructor +Renderer::~Renderer() { + Cleanup(); +} + +// Initialize the renderer +bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers, bool useXR) { + xrMode = useXR; + + // Initialize OpenXR early to get required Vulkan extensions + if (xrMode) { + if (!xrContext.createInstance(appName)) { + std::cerr << "Failed to create OpenXR instance" << std::endl; + return false; + } + } + + // Initialize the Vulkan-Hpp default dispatcher using the global symbol directly. + // This avoids differences across Vulkan-Hpp versions for DynamicLoader placement. + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); + // Create a Vulkan instance + if (!createInstance(appName, enableValidationLayers)) { + std::cerr << "Failed to create Vulkan instance" << std::endl; + return false; + } + + // Setup debug messenger + if (!setupDebugMessenger(enableValidationLayers)) { + std::cerr << "Failed to setup debug messenger" << std::endl; + return false; + } + + // Create surface + if (!createSurface()) { + std::cerr << "Failed to create surface" << std::endl; + return false; + } + + // Pick the physical device + if (!pickPhysicalDevice()) { + std::cerr << "Failed to pick physical device" << std::endl; + return false; + } + + // Create logical device + if (!createLogicalDevice(enableValidationLayers)) { + std::cerr << "Failed to create logical device" << std::endl; + return false; + } + + // Initialize memory pool for efficient memory management + try { + memoryPool = std::make_unique(device, physicalDevice); + if (!memoryPool->initialize()) { + std::cerr << "Failed to initialize memory pool" << std::endl; + return false; + } + + // Optionally pre-allocate initial memory blocks for pools. + // For large scenes (e.g., Bistro) on mid-range GPUs this can cause early OOM. + // Skip pre-allocation to reduce peak memory pressure; blocks will be created on demand. + // if (!memoryPool->preAllocatePools()) { /* non-fatal */ } + } catch (const std::exception& e) { + std::cerr << "Failed to create memory pool: " << e.what() << std::endl; + return false; + } + + // Create swap chain + if (!createSwapChain()) { + std::cerr << "Failed to create swap chain" << std::endl; + return false; + } + + // Create image views + if (!createImageViews()) { + std::cerr << "Failed to create image views" << std::endl; + return false; + } + + // Setup dynamic rendering + if (!setupDynamicRendering()) { + std::cerr << "Failed to setup dynamic rendering" << std::endl; + return false; + } + + // Create the descriptor set layout + if (!createDescriptorSetLayout()) { + std::cerr << "Failed to create descriptor set layout" << std::endl; + return false; + } + + // Create the graphics pipeline + if (!createGraphicsPipeline()) { + std::cerr << "Failed to create graphics pipeline" << std::endl; + return false; + } + + // Create PBR pipeline + if (!createPBRPipeline()) { + std::cerr << "Failed to create PBR pipeline" << std::endl; + return false; + } + + // Create the lighting pipeline + if (!createLightingPipeline()) { + std::cerr << "Failed to create lighting pipeline" << std::endl; + return false; + } + + // Create composite pipeline (fullscreen pass for off-screen → swapchain) + if (!createCompositePipeline()) { + std::cerr << "Failed to create composite pipeline" << std::endl; + return false; + } + + // Create compute pipeline + if (!createComputePipeline()) { + std::cerr << "Failed to create compute pipeline" << std::endl; + return false; + } + + // Ensure light storage buffers exist before creating Forward+ resources + // so that compute descriptor binding 0 (lights SSBO) can be populated safely. + if (!createOrResizeLightStorageBuffers(1)) { + std::cerr << "Failed to create initial light storage buffers" << std::endl; + return false; + } + + // Create Forward+ compute and depth pre-pass pipelines/resources + if (useForwardPlus) { + if (!createForwardPlusPipelinesAndResources()) { + std::cerr << "Failed to create Forward+ resources" << std::endl; + return false; + } + } + + // Create ray query descriptor set layout and pipeline (but not resources yet - need descriptor pool first) + if (!createRayQueryDescriptorSetLayout()) { + std::cerr << "Failed to create ray query descriptor set layout" << std::endl; + return false; + } + if (!createRayQueryPipeline()) { + std::cerr << "Failed to create ray query pipeline" << std::endl; + return false; + } + + // Create the command pool + if (!createCommandPool()) { + std::cerr << "Failed to create command pool" << std::endl; + return false; + } + + // Create depth resources + if (!createDepthResources()) { + std::cerr << "Failed to create depth resources" << std::endl; + return false; + } + + if (useForwardPlus) { + if (!createDepthPrepassPipeline()) { + std::cerr << "Failed to create depth prepass pipeline" << std::endl; + return false; + } + } + + // Create the descriptor pool + if (!createDescriptorPool()) { + std::cerr << "Failed to create descriptor pool" << std::endl; + return false; + } + + // Create ray query resources AFTER descriptor pool (needs pool for descriptor set allocation) + if (!createRayQueryResources()) { + std::cerr << "Failed to create ray query resources" << std::endl; + return false; + } + + // Note: Acceleration structure build is requested by scene_loading.cpp after entities load + // No need to request it here during init + + // Light storage buffers were already created earlier to satisfy Forward+ binding requirements + + if (!createOpaqueSceneColorResources()) { + std::cerr << "Failed to create opaque scene color resources" << std::endl; + return false; + } + + createTransparentDescriptorSets(); + + // Create default texture resources + if (!createDefaultTextureResources()) { + std::cerr << "Failed to create default texture resources" << std::endl; + return false; + } + + // Create fallback transparent descriptor sets (must occur after default textures exist) + createTransparentFallbackDescriptorSets(); + + // Create shared default PBR textures (to avoid creating hundreds of identical textures) + if (!createSharedDefaultPBRTextures()) { + std::cerr << "Failed to create shared default PBR textures" << std::endl; + return false; + } + + // Create command buffers + if (!createCommandBuffers()) { + std::cerr << "Failed to create command buffers" << std::endl; + return false; + } + + // Create sync objects + if (!createSyncObjects()) { + std::cerr << "Failed to create sync objects" << std::endl; + return false; + } + + // Initialize background thread pool for async tasks (textures, etc.) AFTER all Vulkan resources are ready + try { + // Size the thread pool based on hardware concurrency, clamped to a sensible range + unsigned int hw = std::max(2u, std::min(8u, std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4u)); + threadPool = std::make_unique(hw); + } catch (const std::exception& e) { + std::cerr << "Failed to create thread pool: " << e.what() << std::endl; + return false; + } + + // Start background uploads worker now that queues/semaphores exist + StartUploadsWorker(); + + // Start watchdog thread to detect application hangs + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + watchdogRunning.store(true, std::memory_order_relaxed); + watchdogThread = std::thread(WatchdogThreadFunc, &lastFrameUpdateTime, &watchdogRunning, &watchdogSuppressed, &watchdogProgressLabel, &watchdogProgressIndex); + + std::cout << "[Watchdog] Started - will abort if no frame updates for 10+ seconds\n"; + + initialized = true; + return true; +} + +void Renderer::ensureThreadLocalVulkanInit() const { + // Initialize Vulkan-Hpp dispatcher per-thread; required for multi-threaded RAII usage + static thread_local bool s_tlsInitialized = false; + if (s_tlsInitialized) + return; + try { + // Initialize the dispatcher for this thread using the global symbol. + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); + if (*instance) { + VULKAN_HPP_DEFAULT_DISPATCHER.init(*instance); + } + if (*device) { + VULKAN_HPP_DEFAULT_DISPATCHER.init(*device); + } + s_tlsInitialized = true; + } catch (...) { + // best-effort + } +} + +// Clean up renderer resources +void Renderer::Cleanup() { + // Stop watchdog thread first to prevent false hang detection during shutdown + if (watchdogRunning.load(std::memory_order_relaxed)) { + watchdogRunning.store(false, std::memory_order_relaxed); + if (watchdogThread.joinable()) { + watchdogThread.join(); + } + } + + // Ensure background workers are stopped before tearing down Vulkan resources + StopUploadsWorker(); + + // Disallow any further descriptor writes during shutdown. + // This prevents late updates/frees racing against pool destruction. + descriptorSetsValid.store(false, std::memory_order_relaxed); { + std::lock_guard lk(pendingDescMutex); + pendingDescOps.clear(); + descriptorRefreshPending.store(false, std::memory_order_relaxed); + } { + std::unique_lock lock(threadPoolMutex); + if (threadPool) { + threadPool.reset(); + } + } + + if (!initialized) { + return; + } + + std::cout << "Starting renderer cleanup..." << std::endl; + + // Wait for the device to be idle before cleaning up + try { + WaitIdle(); + } catch (...) { + } + + // 1) Clean up any swapchain-scoped resources first + cleanupSwapChain(); + + // 2) Clear per-entity resources (descriptor sets and buffers) while descriptor pools still exist + for (auto& kv : entityResources) { + auto& resources = kv.second; + resources.basicDescriptorSets.clear(); + resources.pbrDescriptorSets.clear(); + resources.uniformBuffers.clear(); + resources.uniformBufferAllocations.clear(); + resources.uniformBuffersMapped.clear(); + resources.instanceBuffer = nullptr; + resources.instanceBufferAllocation = nullptr; + resources.instanceBufferMapped = nullptr; + } + entityResources.clear(); + + // 3) Clear any global descriptor sets that are allocated from pools to avoid dangling refs + transparentDescriptorSets.clear(); + transparentFallbackDescriptorSets.clear(); + compositeDescriptorSets.clear(); + computeDescriptorSets.clear(); + rqCompositeDescriptorSets.clear(); + + // 3.5) Clear ray query descriptor sets BEFORE destroying descriptor pool + // Without this, rayQueryDescriptorSets' RAII destructor tries to free them after + // the pool is destroyed, causing "Invalid VkDescriptorPool Object" validation errors + rayQueryDescriptorSets.clear(); + + // Ray Query composite sampler/sets are allocated from the shared descriptor pool. + // Ensure they are released before destroying the pool. + rqCompositeSampler = nullptr; + + // 4) Destroy/Reset pipelines and pipeline layouts (graphics/compute/forward+) + graphicsPipeline = nullptr; + pbrGraphicsPipeline = nullptr; + pbrBlendGraphicsPipeline = nullptr; + pbrPremulBlendGraphicsPipeline = nullptr; + pbrPrepassGraphicsPipeline = nullptr; + glassGraphicsPipeline = nullptr; + lightingPipeline = nullptr; + compositePipeline = nullptr; + forwardPlusPipeline = nullptr; + depthPrepassPipeline = nullptr; + + pipelineLayout = nullptr; + pbrPipelineLayout = nullptr; + lightingPipelineLayout = nullptr; + compositePipelineLayout = nullptr; + pbrTransparentPipelineLayout = nullptr; + forwardPlusPipelineLayout = nullptr; + + // 4.3) Ray query pipelines and layouts + rayQueryPipeline = nullptr; + rayQueryPipelineLayout = nullptr; + + // 4.5) Forward+ per-frame resources (including descriptor sets) must be released + // BEFORE destroying descriptor pools to avoid vkFreeDescriptorSets with invalid pool + for (auto& fp : forwardPlusPerFrame) { + fp.tileHeaders = nullptr; + fp.tileHeadersAlloc = nullptr; + fp.tileLightIndices = nullptr; + fp.tileLightIndicesAlloc = nullptr; + fp.params = nullptr; + fp.paramsAlloc = nullptr; + fp.paramsMapped = nullptr; + fp.debugOut = nullptr; + fp.debugOutAlloc = nullptr; + fp.probeOffscreen = nullptr; + fp.probeOffscreenAlloc = nullptr; + fp.probeSwapchain = nullptr; + fp.probeSwapchainAlloc = nullptr; + fp.computeSet = nullptr; // descriptor set allocated from compute/graphics pools + } + forwardPlusPerFrame.clear(); + + // 5) Destroy descriptor set layouts and pools (compute + graphics) + descriptorSetLayout = nullptr; + pbrDescriptorSetLayout = nullptr; + transparentDescriptorSetLayout = nullptr; + compositeDescriptorSetLayout = nullptr; + forwardPlusDescriptorSetLayout = nullptr; + computeDescriptorSetLayout = nullptr; + rayQueryDescriptorSetLayout = nullptr; + + // Pools last, after sets are cleared + computeDescriptorPool = nullptr; + descriptorPool = nullptr; + + // 6) Clear textures and aliases, including default resources + { + std::unique_lock lk(textureResourcesMutex); + textureResources.clear(); + textureAliases.clear(); + } + // Reset default texture resources + defaultTextureResources.textureSampler = nullptr; + defaultTextureResources.textureImageView = nullptr; + defaultTextureResources.textureImage = nullptr; + defaultTextureResources.textureImageAllocation = nullptr; + + // 7) Opaque scene color and related descriptors + opaqueSceneColorSampler = nullptr; + opaqueSceneColorImages.clear(); + opaqueSceneColorImageAllocations.clear(); + opaqueSceneColorImageViews.clear(); + opaqueSceneColorImageLayouts.clear(); + + // 7.5) Ray query output image and acceleration structures + rayQueryOutputImageView = nullptr; + rayQueryOutputImage = nullptr; + rayQueryOutputImageAllocation = nullptr; + + // Clear acceleration structures (BLAS and TLAS buffers) + blasStructures.clear(); + tlasStructure = AccelerationStructure{}; + + // 8) (moved above) Forward+ per-frame buffers cleared prior to pool destruction + + // 9) Command buffers/pools + commandBuffers.clear(); + commandPool = nullptr; + computeCommandPool = nullptr; + + // 10) Sync objects + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + uploadsTimeline = nullptr; + + // 11) Queues and surface (RAII handles will release upon reset; keep device alive until the end) + graphicsQueue = nullptr; + presentQueue = nullptr; + computeQueue = nullptr; + transferQueue = nullptr; + surface = nullptr; + + // 12) Memory pool last + memoryPool.reset(); + + // Finally mark uninitialized + initialized = false; + std::cout << "Renderer cleanup completed." << std::endl; +} + +// Create instance +bool Renderer::createInstance(const std::string& appName, bool enableValidationLayers) { + try { + // Create application info + vk::ApplicationInfo appInfo{ + .pApplicationName = appName.c_str(), + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "Simple Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_3 + }; + + // Get required extensions + std::vector extensions; + + // Add required extensions for GLFW +#if defined(PLATFORM_DESKTOP) + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); +#endif + + // Add debug extension if validation layers are enabled + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + // NEW: Add OpenXR mandatory extensions + if (xrMode) { + auto xrExtensions = xrContext.getVulkanInstanceExtensions(); + extensions.insert(extensions.end(), xrExtensions.begin(), xrExtensions.end()); + } + + // Create instance info + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data() + }; + + // Enable validation layers if requested + vk::ValidationFeaturesEXT validationFeatures{}; + std::vector enabledValidationFeatures; + + if (enableValidationLayers) { + if (!checkValidationLayerSupport()) { + std::cerr << "Validation layers requested, but not available" << std::endl; + return false; + } + + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + + // Keep validation output quiet by default (no DebugPrintf feature). + // Ray Query debugPrintf/printf diagnostics are intentionally removed. + + validationFeatures.enabledValidationFeatureCount = static_cast(enabledValidationFeatures.size()); + validationFeatures.pEnabledValidationFeatures = enabledValidationFeatures.data(); + + createInfo.pNext = &validationFeatures; + } + + // Create instance + instance = vk::raii::Instance(context, createInfo); + + if (xrMode) { + xrContext.setVulkanInstance(*instance); + } + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create instance: " << e.what() << std::endl; + return false; + } +} + +// Setup debug messenger +bool Renderer::setupDebugMessenger(bool enableValidationLayers) { + if (!enableValidationLayers) { + return true; + } + + try { + // Create debug messenger info + vk::DebugUtilsMessengerCreateInfoEXT createInfo{}; + createInfo.messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError; + createInfo.messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance; + + // Select callback via simple platform macro: Android typically expects C PFN types in headers + // while desktop (newer Vulkan-Hpp) expects vk:: types. +#if defined(__ANDROID__) + createInfo.pfnUserCallback = &debugCallbackVkRaii; +#else + createInfo.pfnUserCallback = &debugCallbackVkHpp; +#endif + + // Create debug messenger + debugMessenger = vk::raii::DebugUtilsMessengerEXT(instance, createInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to set up debug messenger: " << e.what() << std::endl; + return false; + } +} + +// Create surface +bool Renderer::createSurface() { + try { + // Create surface + VkSurfaceKHR _surface; + if (!platform->CreateVulkanSurface(*instance, &_surface)) { + std::cerr << "Failed to create window surface" << std::endl; + return false; + } + + surface = vk::raii::SurfaceKHR(instance, _surface); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create surface: " << e.what() << std::endl; + return false; + } +} + +// Pick a physical device +bool Renderer::pickPhysicalDevice() { + try { + // Get available physical devices + std::vector devices = instance.enumeratePhysicalDevices(); + + if (devices.empty()) { + std::cerr << "Failed to find GPUs with Vulkan support" << std::endl; + return false; + } + + // Prioritize discrete GPUs (like NVIDIA RTX 2080) over integrated GPUs (like Intel UHD Graphics) + // First, collect all suitable devices with their suitability scores + std::multimap suitableDevices; + + for (auto& _device : devices) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = _device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName + << " (Type: " << vk::to_string(deviceProperties.deviceType) << ")" << std::endl; + + if (xrMode) { + // Match the LUID provided by OpenXR + vk::PhysicalDeviceIDProperties idProps; + vk::PhysicalDeviceProperties2 props2; + props2.pNext = &idProps; + _device.getProperties2(&props2); + + if (std::memcmp(idProps.deviceLUID, xrContext.getRequiredLUID(), VK_LUID_SIZE) != 0) { + std::cout << " - LUID mismatch for OpenXR" << std::endl; + continue; // Not the right GPU for XR! + } + } + + // Check if the device supports Vulkan 1.3 + bool supportsVulkan1_3 = deviceProperties.apiVersion >= VK_API_VERSION_1_3; + if (!supportsVulkan1_3) { + std::cout << " - Does not support Vulkan 1.3" << std::endl; + continue; + } + + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(_device); + bool supportsGraphics = indices.isComplete(); + if (!supportsGraphics) { + std::cout << " - Missing required queue families" << std::endl; + continue; + } + + // Check device extensions + bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(_device); + if (!supportsAllRequiredExtensions) { + std::cout << " - Missing required extensions" << std::endl; + continue; + } + + // Check swap chain support + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(_device); + bool swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) { + std::cout << " - Inadequate swap chain support" << std::endl; + continue; + } + + // Check for required features + auto features = _device.getFeatures2(); + bool supportsRequiredFeatures = features.get().dynamicRendering; + if (!supportsRequiredFeatures) { + std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + continue; + } + + // Calculate suitability score - prioritize discrete GPUs + int score = 0; + + // Discrete GPUs get the highest priority (NVIDIA RTX 2080, AMD, etc.) + if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + score += 1000; + std::cout << " - Discrete GPU: +1000 points" << std::endl; + } + // Integrated GPUs get lower priority (Intel UHD Graphics, etc.) + else if (deviceProperties.deviceType == vk::PhysicalDeviceType::eIntegratedGpu) { + score += 100; + std::cout << " - Integrated GPU: +100 points" << std::endl; + } + + // Add points for memory size (more VRAM is better) + vk::PhysicalDeviceMemoryProperties memProperties = _device.getMemoryProperties(); + for (uint32_t i = 0; i < memProperties.memoryHeapCount; i++) { + if (memProperties.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) { + // Add 1 point per GB of VRAM + score += static_cast(memProperties.memoryHeaps[i].size / (1024 * 1024 * 1024)); + break; + } + } + + std::cout << " - Device is suitable with score: " << score << std::endl; + suitableDevices.emplace(score, _device); + } + + if (!suitableDevices.empty()) { + // Select the device with the highest score (discrete GPU with most VRAM) + physicalDevice = suitableDevices.rbegin()->second; + + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + std::cout << "Selected device: " << deviceProperties.deviceName + << " (Type: " << vk::to_string(deviceProperties.deviceType) + << ", Score: " << suitableDevices.rbegin()->first << ")" << std::endl; + + // Store queue family indices for the selected device + queueFamilyIndices = findQueueFamilies(physicalDevice); + + // Add supported optional extensions + addSupportedOptionalExtensions(); + + return true; + } + std::cerr << "Failed to find a suitable GPU. Make sure your GPU supports Vulkan and has the required extensions." << std::endl; + return false; + } catch (const std::exception& e) { + std::cerr << "Failed to pick physical device: " << e.what() << std::endl; + return false; + } +} + +// Add supported optional extensions +void Renderer::addSupportedOptionalExtensions() { + try { + // Get available extensions + auto availableExtensions = physicalDevice.enumerateDeviceExtensionProperties(); + + // Build a set of available extension names for quick lookup + std::set avail; + for (const auto& e : availableExtensions) { + avail.insert(e.extensionName); + } + + for (const auto& optionalExt : optionalDeviceExtensions) { + if (avail.contains(optionalExt)) { + deviceExtensions.push_back(optionalExt); + std::cout << "Adding optional extension: " << optionalExt << std::endl; + } + } + + // NEW: Add OpenXR mandatory device extensions + if (xrMode) { + auto xrDevExtensions = xrContext.getVulkanDeviceExtensions(*physicalDevice); + for (const auto& ext : xrDevExtensions) { + // Ensure we don't duplicate + if (std::find(deviceExtensions.begin(), deviceExtensions.end(), std::string(ext)) == deviceExtensions.end()) { + deviceExtensions.push_back(ext); + } + } + } + } catch (const std::exception& e) { + std::cerr << "Warning: Failed to add optional extensions: " << e.what() << std::endl; + } +} + +// Create logical device +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + try { + // Create queue create info for each unique queue family + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = { + queueFamilyIndices.graphicsFamily.value(), + queueFamilyIndices.presentFamily.value(), + queueFamilyIndices.computeFamily.value(), + queueFamilyIndices.transferFamily.value() + }; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + // Query supported features before enabling them + auto supportedFeatures = physicalDevice.getFeatures2< + vk::PhysicalDeviceFeatures2, + vk::PhysicalDeviceTimelineSemaphoreFeatures, + vk::PhysicalDeviceVulkanMemoryModelFeatures, + vk::PhysicalDeviceBufferDeviceAddressFeatures, + vk::PhysicalDevice8BitStorageFeatures, + vk::PhysicalDeviceVulkan11Features, + vk::PhysicalDeviceVulkan13Features>(); + + // Verify critical features are supported + const auto& coreSupported = supportedFeatures.get().features; + const auto& timelineSupported = supportedFeatures.get(); + const auto& memoryModelSupported = supportedFeatures.get(); + const auto& bufferAddressSupported = supportedFeatures.get(); + const auto& storage8BitSupported = supportedFeatures.get(); + const auto& vulkan11Supported = supportedFeatures.get(); + const auto& vulkan13Supported = supportedFeatures.get(); + + // Check for required features + if (!coreSupported.samplerAnisotropy || + !timelineSupported.timelineSemaphore || + !memoryModelSupported.vulkanMemoryModel || + !bufferAddressSupported.bufferDeviceAddress || + !vulkan11Supported.shaderDrawParameters || + !vulkan13Supported.dynamicRendering || + !vulkan13Supported.synchronization2) { + throw std::runtime_error("Required Vulkan features not supported by physical device"); + } + + // Enable required features (now verified to be supported) + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = vk::True; + features.features.depthBiasClamp = coreSupported.depthBiasClamp ? vk::True : vk::False; + + // Explicitly configure device features to prevent validation layer warnings + // These features are required by extensions or other features, so we enable them explicitly + + // Timeline semaphore features (required for synchronization2) + vk::PhysicalDeviceTimelineSemaphoreFeatures timelineSemaphoreFeatures; + timelineSemaphoreFeatures.timelineSemaphore = vk::True; + + // NEW: Enable Multiview if in XR mode (Chapter 8) + vk::PhysicalDeviceMultiviewFeatures multiviewFeatures; + multiviewFeatures.multiview = xrMode ? vk::True : vk::False; + multiviewFeatures.pNext = &timelineSemaphoreFeatures; + + // Vulkan memory model features (required for some shader operations) + vk::PhysicalDeviceVulkanMemoryModelFeatures memoryModelFeatures; + memoryModelFeatures.vulkanMemoryModel = vk::True; + memoryModelFeatures.vulkanMemoryModelDeviceScope = memoryModelSupported.vulkanMemoryModelDeviceScope ? vk::True : vk::False; + memoryModelFeatures.pNext = &multiviewFeatures; + + // Buffer device address features (required for some buffer operations) + vk::PhysicalDeviceBufferDeviceAddressFeatures bufferDeviceAddressFeatures; + bufferDeviceAddressFeatures.bufferDeviceAddress = vk::True; + + // 8-bit storage features (required for some shader storage operations) + vk::PhysicalDevice8BitStorageFeatures storage8BitFeatures; + storage8BitFeatures.storageBuffer8BitAccess = storage8BitSupported.storageBuffer8BitAccess ? vk::True : vk::False; + + // Enable Vulkan 1.3 features + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + + // Vulkan 1.1 features: shaderDrawParameters to satisfy SPIR-V DrawParameters capability + vk::PhysicalDeviceVulkan11Features vulkan11Features{}; + vulkan11Features.shaderDrawParameters = vk::True; + // Query extended feature support +#if !defined(PLATFORM_ANDROID) + auto featureChain = physicalDevice.getFeatures2< + vk::PhysicalDeviceFeatures2, + vk::PhysicalDeviceDescriptorIndexingFeatures, + vk::PhysicalDeviceRobustness2FeaturesEXT, + vk::PhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, + vk::PhysicalDeviceShaderTileImageFeaturesEXT, + vk::PhysicalDeviceAccelerationStructureFeaturesKHR, + vk::PhysicalDeviceRayQueryFeaturesKHR>(); + const auto& localReadSupported = featureChain.get(); + const auto& tileImageSupported = featureChain.get(); +#else + auto featureChain = physicalDevice.getFeatures2< + vk::PhysicalDeviceFeatures2, + vk::PhysicalDeviceDescriptorIndexingFeatures, + vk::PhysicalDeviceRobustness2FeaturesEXT, + vk::PhysicalDeviceAccelerationStructureFeaturesKHR, + vk::PhysicalDeviceRayQueryFeaturesKHR>(); +#endif + const auto& coreFeaturesSupported = featureChain.get().features; + const auto& indexingFeaturesSupported = featureChain.get(); + const auto& robust2Supported = featureChain.get(); + const auto& accelerationStructureSupported = featureChain.get(); + const auto& rayQuerySupported = featureChain.get(); + + // Ray Query shader uses indexing into a (large) sampled-image array. + // Some drivers require this core feature to be explicitly enabled. + if (coreFeaturesSupported.shaderSampledImageArrayDynamicIndexing) { + features.features.shaderSampledImageArrayDynamicIndexing = vk::True; + } + + // Prepare descriptor indexing features to enable if supported + vk::PhysicalDeviceDescriptorIndexingFeatures indexingFeaturesEnable{}; + descriptorIndexingEnabled = false; + // Enable non-uniform indexing of sampled image arrays when supported — required for + // `NonUniformResourceIndex()` in the ray-query shader to actually take effect. + if (indexingFeaturesSupported.shaderSampledImageArrayNonUniformIndexing) { + indexingFeaturesEnable.shaderSampledImageArrayNonUniformIndexing = vk::True; + descriptorIndexingEnabled = true; + } + + // These are not strictly required when writing a fully-populated descriptor array, + // but enabling them when available avoids edge-case driver behavior for large arrays. + if (descriptorIndexingEnabled) { + if (indexingFeaturesSupported.descriptorBindingPartiallyBound) { + indexingFeaturesEnable.descriptorBindingPartiallyBound = vk::True; + } + if (indexingFeaturesSupported.descriptorBindingUpdateUnusedWhilePending) { + indexingFeaturesEnable.descriptorBindingUpdateUnusedWhilePending = vk::True; + } + } + // Optionally enable UpdateAfterBind flags when supported (not strictly required for RQ textures) + if (indexingFeaturesSupported.descriptorBindingSampledImageUpdateAfterBind) + indexingFeaturesEnable.descriptorBindingSampledImageUpdateAfterBind = vk::True; + if (indexingFeaturesSupported.descriptorBindingUniformBufferUpdateAfterBind) + indexingFeaturesEnable.descriptorBindingUniformBufferUpdateAfterBind = vk::True; + if (indexingFeaturesSupported.descriptorBindingUpdateUnusedWhilePending) + indexingFeaturesEnable.descriptorBindingUpdateUnusedWhilePending = vk::True; + + // Helper to check if an extension is enabled (using string comparison) + auto hasExtension = [&](const char* name) { + return std::find_if(deviceExtensions.begin(), + deviceExtensions.end(), + [&](const char* ext) { + return std::strcmp(ext, name) == 0; + }) != deviceExtensions.end(); + }; + + // Prepare Robustness2 features if the extension is enabled and device supports + auto hasRobust2 = hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); + vk::PhysicalDeviceRobustness2FeaturesEXT robust2Enable{}; + if (hasRobust2) { + if (robust2Supported.robustBufferAccess2) + robust2Enable.robustBufferAccess2 = vk::True; + if (robust2Supported.robustImageAccess2) + robust2Enable.robustImageAccess2 = vk::True; + if (robust2Supported.nullDescriptor) + robust2Enable.nullDescriptor = vk::True; + } + +#if !defined(PLATFORM_ANDROID) + // Prepare Dynamic Rendering Local Read features if extension is enabled and supported + auto hasLocalRead = hasExtension(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME); + vk::PhysicalDeviceDynamicRenderingLocalReadFeaturesKHR localReadEnable{}; + if (hasLocalRead && localReadSupported.dynamicRenderingLocalRead) { + localReadEnable.dynamicRenderingLocalRead = vk::True; + } + + // Prepare Shader Tile Image features if extension is enabled and supported + auto hasTileImage = hasExtension(VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME); + vk::PhysicalDeviceShaderTileImageFeaturesEXT tileImageEnable{}; + if (hasTileImage) { + if (tileImageSupported.shaderTileImageColorReadAccess) + tileImageEnable.shaderTileImageColorReadAccess = vk::True; + if (tileImageSupported.shaderTileImageDepthReadAccess) + tileImageEnable.shaderTileImageDepthReadAccess = vk::True; + if (tileImageSupported.shaderTileImageStencilReadAccess) + tileImageEnable.shaderTileImageStencilReadAccess = vk::True; + } +#endif + + // Prepare Acceleration Structure features if extension is enabled and supported + auto hasAccelerationStructure = hasExtension(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME); + vk::PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureEnable{}; + if (hasAccelerationStructure && accelerationStructureSupported.accelerationStructure) { + accelerationStructureEnable.accelerationStructure = vk::True; + } + + // Prepare Ray Query features if extension is enabled and supported + auto hasRayQuery = hasExtension(VK_KHR_RAY_QUERY_EXTENSION_NAME); + vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryEnable{}; + if (hasRayQuery && rayQuerySupported.rayQuery) { + rayQueryEnable.rayQuery = vk::True; + } + + // Chain the feature structures together (build pNext chain explicitly) + // Base + features.pNext = &multiviewFeatures; + multiviewFeatures.pNext = &timelineSemaphoreFeatures; + timelineSemaphoreFeatures.pNext = &memoryModelFeatures; + memoryModelFeatures.pNext = &bufferDeviceAddressFeatures; + bufferDeviceAddressFeatures.pNext = &storage8BitFeatures; + storage8BitFeatures.pNext = &vulkan11Features; // link 1.1 first + vulkan11Features.pNext = &vulkan13Features; // then 1.3 features + + // Build tail chain starting at Vulkan 1.3 features + void** tailNext = reinterpret_cast(&vulkan13Features.pNext); + if (descriptorIndexingEnabled) { + *tailNext = &indexingFeaturesEnable; + tailNext = reinterpret_cast(&indexingFeaturesEnable.pNext); + } + if (hasRobust2) { + *tailNext = &robust2Enable; + tailNext = reinterpret_cast(&robust2Enable.pNext); + } +#if !defined(PLATFORM_ANDROID) + if (hasLocalRead) { + *tailNext = &localReadEnable; + tailNext = reinterpret_cast(&localReadEnable.pNext); + } + if (hasTileImage) { + *tailNext = &tileImageEnable; + tailNext = reinterpret_cast(&tileImageEnable.pNext); + } +#endif + if (hasAccelerationStructure) { + *tailNext = &accelerationStructureEnable; + tailNext = reinterpret_cast(&accelerationStructureEnable.pNext); + } + if (hasRayQuery) { + *tailNext = &rayQueryEnable; + tailNext = reinterpret_cast(&rayQueryEnable.pNext); + } + + // Record which features ended up enabled (for runtime decisions/tutorial diagnostics) + robustness2Enabled = hasRobust2 && (robust2Enable.robustBufferAccess2 == vk::True || + robust2Enable.robustImageAccess2 == vk::True || + robust2Enable.nullDescriptor == vk::True); +#if !defined(PLATFORM_ANDROID) + dynamicRenderingLocalReadEnabled = hasLocalRead && (localReadEnable.dynamicRenderingLocalRead == vk::True); + shaderTileImageEnabled = hasTileImage && (tileImageEnable.shaderTileImageColorReadAccess == vk::True || + tileImageEnable.shaderTileImageDepthReadAccess == vk::True || + tileImageEnable.shaderTileImageStencilReadAccess == vk::True); +#else + dynamicRenderingLocalReadEnabled = false; + shaderTileImageEnabled = false; +#endif + accelerationStructureEnabled = hasAccelerationStructure && (accelerationStructureEnable.accelerationStructure == vk::True); + rayQueryEnabled = hasRayQuery && (rayQueryEnable.rayQuery == vk::True); + + // One-time startup diagnostics (Ray Query + texture array indexing) + static bool printedFeatureDiag = false; + if (!printedFeatureDiag) { + printedFeatureDiag = true; + std::cout << "[DeviceFeatures] shaderSampledImageArrayDynamicIndexing=" + << (features.features.shaderSampledImageArrayDynamicIndexing == vk::True ? "ON" : "OFF") + << ", shaderSampledImageArrayNonUniformIndexing=" + << (indexingFeaturesEnable.shaderSampledImageArrayNonUniformIndexing == vk::True ? "ON" : "OFF") + << ", descriptorIndexingEnabled=" + << (descriptorIndexingEnabled ? "true" : "false") + << "\n"; + } + + // Create a device. Device layers are deprecated and ignored, so we + // only configure extensions and features here; validation is enabled + // via instance layers. + vk::DeviceCreateInfo createInfo{ + .pNext = &features, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = nullptr // Using pNext for features + }; + + // Create the logical device + device = vk::raii::Device(physicalDevice, createInfo); + + // After logical device is created, we can initialize the OpenXR session + if (xrMode) { + if (!xrContext.createSession(*physicalDevice, *device, queueFamilyIndices.graphicsFamily.value(), 0)) { + std::cerr << "Failed to create OpenXR session" << std::endl; + return false; + } + } + + // Get queue handles + graphicsQueue = vk::raii::Queue(device, queueFamilyIndices.graphicsFamily.value(), 0); + presentQueue = vk::raii::Queue(device, queueFamilyIndices.presentFamily.value(), 0); + computeQueue = vk::raii::Queue(device, queueFamilyIndices.computeFamily.value(), 0); + transferQueue = vk::raii::Queue(device, queueFamilyIndices.transferFamily.value(), 0); + + // Create global timeline semaphore for uploads early (needed before default texture creation) + vk::StructureChain timelineChain( + {}, + {.semaphoreType = vk::SemaphoreType::eTimeline, .initialValue = 0}); + uploadsTimeline = vk::raii::Semaphore(device, timelineChain.get()); + uploadTimelineLastSubmitted.store(0, std::memory_order_relaxed); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create logical device: " << e.what() << std::endl; + return false; + } +} + +// Check validation layer support +bool Renderer::checkValidationLayerSupport() const { + // Get available layers + std::vector availableLayers = context.enumerateInstanceLayerProperties(); + + // Check if all requested layers are available + for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } + } + + return true; +} diff --git a/attachments/openxr_engine/renderer_pipelines.cpp b/attachments/openxr_engine/renderer_pipelines.cpp new file mode 100644 index 000000000..e9dccc9b1 --- /dev/null +++ b/attachments/openxr_engine/renderer_pipelines.cpp @@ -0,0 +1,1412 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mesh_component.h" +#include "renderer.h" +#include +#include +#include + +// This file contains pipeline-related methods from the Renderer class + +// Create a descriptor set layout +bool Renderer::createDescriptorSetLayout() { + try { + // Create binding for a uniform buffer + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }; + + // Create binding for texture sampler + vk::DescriptorSetLayoutBinding samplerLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }; + + // Create a descriptor set layout + std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; + + // Descriptor indexing: set per-binding flags for UPDATE_AFTER_BIND if enabled + vk::DescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{}; + std::array bindingFlags{}; + if (descriptorIndexingEnabled) { + bindingFlags[0] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + bindingFlags[1] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + bindingFlagsInfo.bindingCount = static_cast(bindingFlags.size()); + bindingFlagsInfo.pBindingFlags = bindingFlags.data(); + } + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + if (descriptorIndexingEnabled) { + layoutInfo.flags |= vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + layoutInfo.pNext = &bindingFlagsInfo; + } + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +// Create PBR descriptor set layout +bool Renderer::createPBRDescriptorSetLayout() { + try { + // Create descriptor set layout bindings for PBR shader + std::array bindings = { + // Binding 0: Uniform buffer (UBO) + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 1: Base color map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 2: Metallic roughness map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 3: Normal map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 4: Occlusion map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 4, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 5: Emissive map and sampler + vk::DescriptorSetLayoutBinding{ + .binding = 5, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 6: Light storage buffer (shadows removed) + vk::DescriptorSetLayoutBinding{ + .binding = 6, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 7: Forward+ tile headers SSBO + vk::DescriptorSetLayoutBinding{ + .binding = 7, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 8: Forward+ tile light indices SSBO + vk::DescriptorSetLayoutBinding{ + .binding = 8, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 9: Fragment debug output buffer (optional) + vk::DescriptorSetLayoutBinding{ + .binding = 9, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 10: Reflection texture (planar reflections) + vk::DescriptorSetLayoutBinding{ + .binding = 10, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 11: TLAS (ray-query shadows in raster fragment shader) + vk::DescriptorSetLayoutBinding{ + .binding = 11, + .descriptorType = vk::DescriptorType::eAccelerationStructureKHR, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 12: Ray-query geometry info buffer (per-instance addresses + material indices) + vk::DescriptorSetLayoutBinding{ + .binding = 12, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + }, + // Binding 13: Ray-query material buffer (PBR material properties) + vk::DescriptorSetLayoutBinding{ + .binding = 13, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr + } + }; + + // Create a descriptor set layout + // Descriptor indexing: set per-binding flags for UPDATE_AFTER_BIND on UBO (0) and sampled images (1..5) + vk::DescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{}; + std::array bindingFlags{}; + if (descriptorIndexingEnabled) { + bindingFlags[0] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + bindingFlags[1] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + bindingFlags[10] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + bindingFlagsInfo.bindingCount = static_cast(bindingFlags.size()); + bindingFlagsInfo.pBindingFlags = bindingFlags.data(); + } + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + if (descriptorIndexingEnabled) { + layoutInfo.flags |= vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + layoutInfo.pNext = &bindingFlagsInfo; + } + + pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + // Binding 7: transparent passes input + // Layout for Set 1: Just the scene color texture + vk::DescriptorSetLayoutBinding sceneColorBinding{ + .binding = 0, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment + }; + vk::DescriptorSetLayoutCreateInfo transparentLayoutInfo{.bindingCount = 1, .pBindings = &sceneColorBinding}; + if (descriptorIndexingEnabled) { + // Make this sampler binding update-after-bind safe as well (optional) + vk::DescriptorSetLayoutBindingFlagsCreateInfo transBindingFlagsInfo{}; + vk::DescriptorBindingFlags transFlags = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + transBindingFlagsInfo.bindingCount = 1; + transBindingFlagsInfo.pBindingFlags = &transFlags; + transparentLayoutInfo.flags |= vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + transparentLayoutInfo.pNext = &transBindingFlagsInfo; + + // Create the layout while the pNext chain is still valid (avoid dangling pointer) + transparentDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, transparentLayoutInfo); + } else { + // Create without extra binding flags + transparentDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, transparentLayoutInfo); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +// Create a graphics pipeline +bool Renderer::createGraphicsPipeline() { + try { + // Read shader code + auto shaderCode = readFile("shaders/texturedMesh.spv"); + + // Create shader modules + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *shaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "PSMain" + }; + + // Fragment entry point specialized for architectural glass + vk::PipelineShaderStageCreateInfo fragGlassStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "GlassPSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info with instancing support + auto vertexBindingDescription = Vertex::getBindingDescription(); + auto instanceBindingDescription = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription + }; + + auto vertexAttributeDescriptions = Vertex::getAttributeDescriptions(); + auto instanceAttributeDescriptions = InstanceData::getAttributeDescriptions(); + + // Combine all attribute descriptions (no duplicates) + std::vector allAttributeDescriptions; + allAttributeDescriptions.insert(allAttributeDescriptions.end(), vertexAttributeDescriptions.begin(), vertexAttributeDescriptions.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceAttributeDescriptions.begin(), instanceAttributeDescriptions.end()); + + // Note: materialIndex attribute (Location 11) is not used by current shaders + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .vertexAttributeDescriptionCount = static_cast(allAttributeDescriptions.size()), + .pVertexAttributeDescriptions = allAttributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eNone, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + // Use LessOrEqual so that the main shading pass works after a depth pre-pass + .depthCompareOp = vk::CompareOp::eLessOrEqual, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create a color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr + }; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create pipeline rendering info + vk::Format depthFormat = findDepthFormat(); + std::cout << "Creating main graphics pipeline with depth format: " << static_cast(depthFormat) << std::endl; + + // Initialize member variable for proper lifetime management + mainPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + // Create the graphics pipeline + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + // Disable back-face culling for opaque PBR to avoid disappearing geometry when + // instance/model transforms flip winding (ensures PASS 1 actually shades pixels) + rasterizerBack.cullMode = vk::CullModeFlagBits::eNone; + + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &mainPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerBack, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create PBR pipeline +bool Renderer::createPBRPipeline() { + try { + // Create PBR descriptor set layout + if (!createPBRDescriptorSetLayout()) { + return false; + } + + // Read shader code + auto shaderCode = readFile("shaders/pbr.spv"); + + // Create shader modules + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *shaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "PSMain" + }; + + // Fragment entry point specialized for architectural glass + vk::PipelineShaderStageCreateInfo fragGlassStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "GlassPSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Define vertex and instance binding descriptions + auto vertexBindingDescription = Vertex::getBindingDescription(); + auto instanceBindingDescription = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription + }; + + // Define vertex and instance attribute descriptions + auto vertexAttributeDescriptions = Vertex::getAttributeDescriptions(); + auto instanceModelMatrixAttributes = InstanceData::getModelMatrixAttributeDescriptions(); + auto instanceNormalMatrixAttributes = InstanceData::getNormalMatrixAttributeDescriptions(); + + // Combine all attribute descriptions + std::vector allAttributeDescriptions; + allAttributeDescriptions.insert(allAttributeDescriptions.end(), vertexAttributeDescriptions.begin(), vertexAttributeDescriptions.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceModelMatrixAttributes.begin(), instanceModelMatrixAttributes.end()); + allAttributeDescriptions.insert(allAttributeDescriptions.end(), instanceNormalMatrixAttributes.begin(), instanceNormalMatrixAttributes.end()); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .vertexAttributeDescriptionCount = static_cast(allAttributeDescriptions.size()), + .pVertexAttributeDescriptions = allAttributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eNone, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create a color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + + std::array transparentSetLayouts = {*pbrDescriptorSetLayout, *transparentDescriptorSetLayout}; + // Create a pipeline layout for opaque PBR with only the PBR descriptor set (set 0) + std::array pbrOnlySetLayouts = {*pbrDescriptorSetLayout}; + // Create BOTH pipeline layouts with two descriptor sets (PBR set 0 + scene color set 1) + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = static_cast(transparentSetLayouts.size()), + .pSetLayouts = transparentSetLayouts.data(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Transparent PBR layout uses the same two-set layout + vk::PipelineLayoutCreateInfo transparentPipelineLayoutInfo{.setLayoutCount = static_cast(transparentSetLayouts.size()), .pSetLayouts = transparentSetLayouts.data(), .pushConstantRangeCount = 1, .pPushConstantRanges = &pushConstantRange}; + pbrTransparentPipelineLayout = vk::raii::PipelineLayout(device, transparentPipelineLayoutInfo); + + // Create pipeline rendering info + vk::Format depthFormat = findDepthFormat(); + + // Initialize member variable for proper lifetime management + pbrPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + // 1) Opaque PBR pipeline (no blending, depth writes enabled) + vk::PipelineColorBlendAttachmentState opaqueBlendAttachment = colorBlendAttachment; + opaqueBlendAttachment.blendEnable = VK_FALSE; + vk::PipelineColorBlendStateCreateInfo colorBlendingOpaque{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &opaqueBlendAttachment + }; + vk::PipelineDepthStencilStateCreateInfo depthStencilOpaque = depthStencil; + depthStencilOpaque.depthWriteEnable = VK_TRUE; + + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; + + // For architectural glass we often want to see both the inner and outer + // walls of thin shells (e.g., bar glasses viewed from above). Use + // no culling for the glass pipeline to render both sides, while + // keeping back-face culling for the generic PBR pipelines. + vk::PipelineRasterizationStateCreateInfo rasterizerGlass = rasterizer; + rasterizerGlass.cullMode = vk::CullModeFlagBits::eNone; + + vk::GraphicsPipelineCreateInfo opaquePipelineInfo{ + + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerBack, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilOpaque, + .pColorBlendState = &colorBlendingOpaque, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrGraphicsPipeline = vk::raii::Pipeline(device, nullptr, opaquePipelineInfo); + + // 1b) Opaque PBR pipeline variant for color pass after a depth pre-pass. + // Depth writes disabled (read-only) and compare against pre-pass depth. + vk::PipelineDepthStencilStateCreateInfo depthStencilAfterPrepass = depthStencil; + depthStencilAfterPrepass.depthTestEnable = VK_TRUE; + depthStencilAfterPrepass.depthWriteEnable = VK_FALSE; + depthStencilAfterPrepass.depthCompareOp = vk::CompareOp::eEqual; + + vk::GraphicsPipelineCreateInfo opaqueAfterPrepassInfo{ + + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerBack, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilAfterPrepass, + .pColorBlendState = &colorBlendingOpaque, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrPrepassGraphicsPipeline = vk::raii::Pipeline(device, nullptr, opaqueAfterPrepassInfo); + + // 1c) Reflection PBR pipeline for mirrored off-screen pass (cull none to avoid winding issues) + vk::PipelineRasterizationStateCreateInfo rasterizerReflection = rasterizer; + rasterizerReflection.cullMode = vk::CullModeFlagBits::eNone; + vk::GraphicsPipelineCreateInfo reflectionPipelineInfo{ + + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerReflection, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilOpaque, + .pColorBlendState = &colorBlendingOpaque, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrReflectionGraphicsPipeline = vk::raii::Pipeline(device, nullptr, reflectionPipelineInfo); + + // 2) Blended PBR pipeline (straight alpha blending, depth writes disabled for translucency) + vk::PipelineColorBlendAttachmentState blendedAttachment = colorBlendAttachment; + blendedAttachment.blendEnable = VK_TRUE; + // Straight alpha blending: out.rgb = src.rgb*src.a + dst.rgb*(1-src.a) + blendedAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; + blendedAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + // Alpha channel keeps destination scaled by inverse src alpha + blendedAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; + blendedAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + vk::PipelineColorBlendStateCreateInfo colorBlendingBlended{.attachmentCount = 1, .pAttachments = &blendedAttachment}; + vk::PipelineDepthStencilStateCreateInfo depthStencilBlended = depthStencil; + depthStencilBlended.depthWriteEnable = VK_FALSE; + depthStencilBlended.depthCompareOp = vk::CompareOp::eLessOrEqual; + + vk::GraphicsPipelineCreateInfo blendedPipelineInfo{ + + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + // Use back-face culling for the blended (glass) pipeline to avoid + // rendering both front and back faces of thin glass geometry, which + // can cause flickering as the camera rotates due to overlapping + // transparent surfaces passing the depth test. + .pRasterizationState = &rasterizerBack, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilBlended, + .pColorBlendState = &colorBlendingBlended, + .pDynamicState = &dynamicState, + .layout = *pbrTransparentPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrBlendGraphicsPipeline = vk::raii::Pipeline(device, nullptr, blendedPipelineInfo); + + // 3) Glass pipeline (architectural glass) - uses the same vertex input and + // descriptor layouts, but a dedicated fragment shader entry point + // (GlassPSMain) for more stable glass shading. + vk::PipelineShaderStageCreateInfo glassStages[] = {vertShaderStageInfo, fragGlassStageInfo}; + + vk::GraphicsPipelineCreateInfo glassPipelineInfo{ + + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = glassStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerGlass, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilBlended, + .pColorBlendState = &colorBlendingBlended, + .pDynamicState = &dynamicState, + .layout = *pbrTransparentPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + glassGraphicsPipeline = vk::raii::Pipeline(device, nullptr, glassPipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create fullscreen composite pipeline (samples off-screen color and writes to swapchain) +bool Renderer::createCompositePipeline() { + try { + // Reuse the transparent descriptor set layout (binding 0 = combined image sampler) + if (*transparentDescriptorSetLayout == nullptr) { + // Ensure PBR pipeline path created it + if (!createPBRPipeline()) { + return false; + } + } + + // Read composite shader code + auto shaderCode = readFile("shaders/composite.spv"); + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Shader stages + vk::PipelineShaderStageCreateInfo vert{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *shaderModule, + .pName = "VSMain" + }; + vk::PipelineShaderStageCreateInfo frag{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "PSMain" + }; + vk::PipelineShaderStageCreateInfo stages[] = {vert, frag}; + + // No vertex inputs (fullscreen triangle via SV_VertexID) + vk::PipelineVertexInputStateCreateInfo vertexInput{}; + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{.topology = vk::PrimitiveTopology::eTriangleList}; + vk::PipelineViewportStateCreateInfo viewportState{.viewportCount = 1, .scissorCount = 1}; + vk::PipelineRasterizationStateCreateInfo rasterizer{.polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .lineWidth = 1.0f}; + vk::PipelineMultisampleStateCreateInfo multisampling{.rasterizationSamples = vk::SampleCountFlagBits::e1}; + // No depth + vk::PipelineDepthStencilStateCreateInfo depthStencil{.depthTestEnable = VK_FALSE, .depthWriteEnable = VK_FALSE}; + // No blending (we clear swapchain before this and blend transparents later) + vk::PipelineColorBlendAttachmentState attachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + vk::PipelineColorBlendStateCreateInfo colorBlending{.attachmentCount = 1, .pAttachments = &attachment}; + std::array dynStates = {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; + vk::PipelineDynamicStateCreateInfo dynamicState{.dynamicStateCount = static_cast(dynStates.size()), .pDynamicStates = dynStates.data()}; + + // Pipeline layout: single set (combined image sampler) + push constants for exposure/gamma/srgb flag + vk::DescriptorSetLayout setLayouts[] = {*transparentDescriptorSetLayout}; + vk::PushConstantRange pushRange{.stageFlags = vk::ShaderStageFlagBits::eFragment, .offset = 0, .size = 16}; // matches struct Push in composite.slang + vk::PipelineLayoutCreateInfo plInfo{.setLayoutCount = 1, .pSetLayouts = setLayouts, .pushConstantRangeCount = 1, .pPushConstantRanges = &pushRange}; + compositePipelineLayout = vk::raii::PipelineLayout(device, plInfo); + + // Dynamic rendering info + compositePipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = vk::Format::eUndefined, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + vk::GraphicsPipelineCreateInfo pipeInfo{ + + .pNext = &compositePipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = stages, + .pVertexInputState = &vertexInput, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *compositePipelineLayout, + .renderPass = nullptr, + .subpass = 0 + }; + + compositePipeline = vk::raii::Pipeline(device, nullptr, pipeInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create composite pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create Depth Pre-pass pipeline (depth-only) +bool Renderer::createDepthPrepassPipeline() { + try { + // Use the same descriptor set layout and pipeline layout as PBR for UBOs and instancing + if (*pbrDescriptorSetLayout == nullptr || *pbrPipelineLayout == nullptr) { + if (!createPBRPipeline()) { + return false; + } + } + + // Read PBR shader (vertex only) + auto shaderCode = readFile("shaders/pbr.spv"); + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Stages: Vertex only + vk::PipelineShaderStageCreateInfo vertStage{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *shaderModule, + .pName = "VSMain" + }; + + // Vertex/instance bindings & attributes same as PBR + auto vertexBindingDescription = Vertex::getBindingDescription(); + auto instanceBindingDescription = InstanceData::getBindingDescription(); + std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription + }; + + auto vertexAttributeDescriptions = Vertex::getAttributeDescriptions(); + auto instanceModelMatrixAttributes = InstanceData::getModelMatrixAttributeDescriptions(); + auto instanceNormalMatrixAttributes = InstanceData::getNormalMatrixAttributeDescriptions(); + std::vector allAttributes; + allAttributes.insert(allAttributes.end(), vertexAttributeDescriptions.begin(), vertexAttributeDescriptions.end()); + allAttributes.insert(allAttributes.end(), instanceModelMatrixAttributes.begin(), instanceModelMatrixAttributes.end()); + allAttributes.insert(allAttributes.end(), instanceNormalMatrixAttributes.begin(), instanceNormalMatrixAttributes.end()); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .vertexAttributeDescriptionCount = static_cast(allAttributes.size()), + .pVertexAttributeDescriptions = allAttributes.data() + }; + + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Dummy viewport/scissor (dynamic) + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1 + }; + + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLessOrEqual, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // No color attachments + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .attachmentCount = 0, + .pAttachments = nullptr + }; + + std::array dynamicStates = {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + vk::Format depthFormat = findDepthFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, + .colorAttachmentCount = 0, + .pColorAttachmentFormats = nullptr, + .depthAttachmentFormat = depthFormat + }; + + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &renderingInfo, + .stageCount = 1, + .pStages = &vertStage, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout + }; + + depthPrepassPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create depth pre-pass pipeline: " << e.what() << std::endl; + return false; + } +} + +// Create a lighting pipeline +bool Renderer::createLightingPipeline() { + try { + // Read shader code + auto shaderCode = readFile("shaders/lighting.spv"); + + // Create shader modules + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *shaderModule, + .pName = "VSMain" + }; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *shaderModule, + .pName = "PSMain" + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescriptions = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), + .pVertexAttributeDescriptions = attributeDescriptions.data() + }; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE + }; + + // Create viewport state info + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .scissorCount = 1 + }; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eNone, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f + }; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE + }; + + // Create depth stencil state info + vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE + }; + + // Create a color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_TRUE, + .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, + .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, + .colorBlendOp = vk::BlendOp::eAdd, + .srcAlphaBlendFactor = vk::BlendFactor::eOne, + .dstAlphaBlendFactor = vk::BlendFactor::eZero, + .alphaBlendOp = vk::BlendOp::eAdd, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + // Create color blend state info + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Create dynamic state info + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState{ + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + // Create push constant range for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(MaterialProperties) + }; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + lightingPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create pipeline rendering info + vk::Format depthFormat = findDepthFormat(); + + // Initialize member variable for proper lifetime management + lightingPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = vk::Format::eUndefined + }; + + // Create a graphics pipeline + vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; + rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; + + vk::GraphicsPipelineCreateInfo pipelineInfo{ + + .pNext = &lightingPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizerBack, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *lightingPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + + lightingPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create lighting pipeline: " << e.what() << std::endl; + return false; + } +} + +// Push material properties to the pipeline +void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) const { + commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(MaterialProperties), &material); +} + +bool Renderer::createRayQueryDescriptorSetLayout() { + // Production layout: 7 bindings (0..6), no debug buffer at 7 + std::array bindings{}; + + // Binding 0: UBO (UniformBufferObject) + bindings[0].binding = 0; + bindings[0].descriptorType = vk::DescriptorType::eUniformBuffer; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 1: TLAS (Top-Level Acceleration Structure) + bindings[1].binding = 1; + bindings[1].descriptorType = vk::DescriptorType::eAccelerationStructureKHR; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 2: Output image (storage image) + bindings[2].binding = 2; + bindings[2].descriptorType = vk::DescriptorType::eStorageImage; + bindings[2].descriptorCount = 1; + bindings[2].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 3: Light buffer (storage buffer) + bindings[3].binding = 3; + bindings[3].descriptorType = vk::DescriptorType::eStorageBuffer; + bindings[3].descriptorCount = 1; + bindings[3].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 4: Geometry info buffer (maps BLAS geometry index to vertex/index buffer addresses) + bindings[4].binding = 4; + bindings[4].descriptorType = vk::DescriptorType::eStorageBuffer; + bindings[4].descriptorCount = 1; + bindings[4].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 5: Material buffer (array of material properties) + bindings[5].binding = 5; + bindings[5].descriptorType = vk::DescriptorType::eStorageBuffer; + bindings[5].descriptorCount = 1; + bindings[5].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Binding 6: BaseColor textures array (combined image samplers) + bindings[6].binding = 6; + bindings[6].descriptorType = vk::DescriptorType::eCombinedImageSampler; + bindings[6].descriptorCount = RQ_MAX_TEX; // large static array + bindings[6].stageFlags = vk::ShaderStageFlagBits::eCompute; + + // Descriptor indexing / update-after-bind support: + // The ray query shader indexes a large `eCombinedImageSampler` array with a per-pixel varying index. + // On some drivers this requires descriptor indexing features + layout binding flags to avoid the + // array collapsing to slot 0 (resulting in "no textures" even when `texIndex>0`). + std::array bindingFlags{}; + if (descriptorIndexingEnabled) { + // Binding 6 is the large sampled texture array. + bindingFlags[6] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | + vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending | + vk::DescriptorBindingFlagBits::ePartiallyBound; + } + + vk::DescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{}; + if (descriptorIndexingEnabled) { + bindingFlagsInfo.bindingCount = static_cast(bindingFlags.size()); + bindingFlagsInfo.pBindingFlags = bindingFlags.data(); + } + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + if (descriptorIndexingEnabled) { + layoutInfo.pNext = &bindingFlagsInfo; + layoutInfo.flags = vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + } + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + try { + rayQueryDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create ray query descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createRayQueryPipeline() { + // Check if ray query is supported on this device + if (!rayQueryEnabled || !accelerationStructureEnabled) { + std::cout << "Ray query rendering not available on this device (missing VK_KHR_ray_query or VK_KHR_acceleration_structure support)\n"; + return true; // Not an error - just skip ray query pipeline creation + } + + // Load compiled shader module + auto shaderCode = readFile("shaders/ray_query.spv"); + if (shaderCode.empty()) { + std::cerr << "Failed to load ray query shader\n"; + return false; + } + + vk::ShaderModuleCreateInfo createInfo{}; + createInfo.codeSize = shaderCode.size(); + createInfo.pCode = reinterpret_cast(shaderCode.data()); + + vk::raii::ShaderModule shaderModule(device, createInfo); + + vk::PipelineShaderStageCreateInfo shaderStage{}; + shaderStage.stage = vk::ShaderStageFlagBits::eCompute; + shaderStage.module = *shaderModule; + shaderStage.pName = "main"; + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &(*rayQueryDescriptorSetLayout); + + rayQueryPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create compute pipeline + vk::ComputePipelineCreateInfo pipelineInfo{}; + pipelineInfo.stage = shaderStage; + pipelineInfo.layout = *rayQueryPipelineLayout; + + try { + rayQueryPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create ray query pipeline: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createRayQueryResources() { + try { + // Create output image using memory pool (storage image for compute shader) + // Use an HDR-capable format for Ray Query so PBR lighting can accumulate in linear space + // before composite applies exposure/gamma. + // Fall back to R8G8B8A8_UNORM if the device does not support storage-image usage. + vk::Format rqFormat = vk::Format::eR16G16B16A16Sfloat; { + auto props = physicalDevice.getFormatProperties(rqFormat); + if (!(props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eStorageImage)) { + rqFormat = vk::Format::eR8G8B8A8Unorm; + } + } + auto [image, allocation] = memoryPool->createImage( + swapChainExtent.width, + swapChainExtent.height, + rqFormat, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal, + 1, + // mipLevels + vk::SharingMode::eExclusive, + {} // queueFamilies + ); + + rayQueryOutputImage = std::move(image); + rayQueryOutputImageAllocation = std::move(allocation); + + // Create image view + vk::ImageViewCreateInfo viewInfo{}; + viewInfo.image = *rayQueryOutputImage; + viewInfo.viewType = vk::ImageViewType::e2D; + viewInfo.format = rqFormat; + viewInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + rayQueryOutputImageView = vk::raii::ImageView(device, viewInfo); + + // Transition output image to GENERAL layout for compute shader writes + transitionImageLayout(*rayQueryOutputImage, + rqFormat, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eGeneral, + 1); + + // Allocate descriptor sets (one per frame in flight) + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *rayQueryDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; + allocInfo.pSetLayouts = layouts.data(); + + // Allocate into a temporary owning container, then move the individual RAII sets into our vector. + // (Avoid assigning `vk::raii::DescriptorSets` directly into `std::vector`.) + { + auto sets = vk::raii::DescriptorSets(device, allocInfo); + rayQueryDescriptorSets.clear(); + rayQueryDescriptorSets.reserve(sets.size()); + for (auto& s : sets) { + rayQueryDescriptorSets.emplace_back(std::move(s)); + } + } + + // Create descriptor sets for composite pass to sample the rayQueryOutputImage + // Reuse the transparentDescriptorSetLayout (binding 0 = combined image sampler) + if (*transparentDescriptorSetLayout == nullptr) { + // Ensure it exists (created by PBR path); + createPBRPipeline(); + } + if (*transparentDescriptorSetLayout != nullptr) { + // Ensure we have a valid sampler for sampling the ray-query output image + if (*rqCompositeSampler == nullptr) { + vk::SamplerCreateInfo sci{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eNearest, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_FALSE, + .compareOp = vk::CompareOp::eAlways, + .minLod = 0.0f, + .maxLod = 0.0f, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = VK_FALSE + }; + rqCompositeSampler = vk::raii::Sampler(device, sci); + } + std::vector rqLayouts(MAX_FRAMES_IN_FLIGHT, *transparentDescriptorSetLayout); + vk::DescriptorSetAllocateInfo rqAllocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, + .pSetLayouts = rqLayouts.data() + }; { + auto sets = vk::raii::DescriptorSets(device, rqAllocInfo); + rqCompositeDescriptorSets.clear(); + rqCompositeDescriptorSets.reserve(sets.size()); + for (auto& s : sets) { + rqCompositeDescriptorSets.emplace_back(std::move(s)); + } + } + + // Update each set to sample the rayQueryOutputImage + for (size_t i = 0; i < rqCompositeDescriptorSets.size(); ++i) { + // Use a dedicated sampler to avoid null sampler issues during early init + vk::Sampler samplerHandle = *rqCompositeSampler; + vk::DescriptorImageInfo imgInfo{ + .sampler = samplerHandle, + .imageView = *rayQueryOutputImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + vk::WriteDescriptorSet write{ + .dstSet = *rqCompositeDescriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imgInfo + }; + device.updateDescriptorSets({write}, {}); + } + } + + // Create dedicated UBO buffers for ray query (one per frame in flight) + rayQueryUniformBuffers.clear(); + rayQueryUniformAllocations.clear(); + rayQueryUniformBuffersMapped.clear(); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + auto [uboBuffer, uboAlloc] = createBufferPooled( + sizeof(RayQueryUniformBufferObject), + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + rayQueryUniformBuffers.push_back(std::move(uboBuffer)); + rayQueryUniformAllocations.push_back(std::move(uboAlloc)); + rayQueryUniformBuffersMapped.push_back(rayQueryUniformAllocations.back()->mappedPtr); + } + + std::cout << "Ray query resources created successfully (including " << MAX_FRAMES_IN_FLIGHT << " dedicated UBOs)\n"; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create ray query resources: " << e.what() << std::endl; + return false; + } +} diff --git a/attachments/openxr_engine/renderer_rendering.cpp b/attachments/openxr_engine/renderer_rendering.cpp new file mode 100644 index 000000000..e129d70b4 --- /dev/null +++ b/attachments/openxr_engine/renderer_rendering.cpp @@ -0,0 +1,2963 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "imgui/imgui.h" +#include "imgui_system.h" +#include "mesh_component.h" +#include "model_loader.h" +#include "renderer.h" +#include "transform_component.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== Culling helpers implementation ===================== + +Renderer::FrustumPlanes Renderer::extractFrustumPlanes(const glm::mat4& vp) { + // Work in row-major form for standard plane extraction by transposing GLM's column-major matrix + glm::mat4 m = glm::transpose(vp); + FrustumPlanes fp{}; + // Left : m[3] + m[0] + fp.planes[0] = m[3] + m[0]; + // Right : m[3] - m[0] + fp.planes[1] = m[3] - m[0]; + // Bottom : m[3] + m[1] + fp.planes[2] = m[3] + m[1]; + // Top : m[3] - m[1] + fp.planes[3] = m[3] - m[1]; + // Near : m[2] (matches Vulkan [0, 1] clip range) + fp.planes[4] = m[2]; + // Far : m[3] - m[2] + fp.planes[5] = m[3] - m[2]; + + // Normalize planes + for (auto& p : fp.planes) { + glm::vec3 n(p.x, p.y, p.z); + float len = glm::length(n); + if (len > 0.0f) { + p /= len; + } + } + return fp; +} + +void Renderer::transformAABB(const glm::mat4& M, + const glm::vec3& localMin, + const glm::vec3& localMax, + glm::vec3& outMin, + glm::vec3& outMax) { + // OBB (from model) to world AABB using center/extents and absolute 3x3 + const glm::vec3 c = 0.5f * (localMin + localMax); + const glm::vec3 e = 0.5f * (localMax - localMin); + + const glm::vec3 worldCenter = glm::vec3(M * glm::vec4(c, 1.0f)); + // Upper-left 3x3 + const glm::mat3 A = glm::mat3(M); + const glm::mat3 AbsA = glm::mat3(glm::abs(A[0]), glm::abs(A[1]), glm::abs(A[2])); + const glm::vec3 worldExtents = AbsA * e; // component-wise combination + + outMin = worldCenter - worldExtents; + outMax = worldCenter + worldExtents; +} + +bool Renderer::aabbIntersectsFrustum(const glm::vec3& worldMin, + const glm::vec3& worldMax, + const FrustumPlanes& frustum) { + // Use the p-vertex test against each plane; if outside any plane → culled + for (const auto& p : frustum.planes) { + const glm::vec3 n(p.x, p.y, p.z); + // Choose positive vertex (furthest in direction of normal) + glm::vec3 v{ + n.x >= 0.0f ? worldMax.x : worldMin.x, + n.y >= 0.0f ? worldMax.y : worldMin.y, + n.z >= 0.0f ? worldMax.z : worldMin.z + }; + + // If the most positive vertex is still on the negative side of the plane, + // then the entire box is on the negative side. + // Use a small epsilon to avoid numerical issues. + if (glm::dot(n, v) + p.w < -0.01f) { + return false; // completely outside + } + } + return true; +} + +// This file contains rendering-related methods from the Renderer class + +// Create swap chain +bool Renderer::createSwapChain() { + try { + if (xrMode) { + // NEW: Negotiate with OpenXR instead of the windowing system + // Query swap chain support (though XR often dictates this) + vk::Extent2D xrExtent = xrContext.getRecommendedExtent(); + xrContext.createSwapchains(*device, vk::Format::eB8G8R8A8Srgb, xrExtent); + + swapChainImageFormat = xrContext.getSwapchainFormat(); + swapChainExtent = xrContext.getSwapchainExtent(); + + // Use a single swapchain with 2 layers for multiview + eyeSwapchainImages[0] = xrContext.enumerateSwapchainImages(); + eyeSwapchainImages[1].clear(); // Not used in multiview mode + + // We still use swapChainImages as a dummy or to track common state if needed + return true; + } + + // Query swap chain support + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + // Choose swap surface format, present mode, and extent + vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities); + + // Choose image count + uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; + if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; + } + + // Create swap chain info + vk::SwapchainCreateInfoKHR createInfo{ + .surface = *surface, + .minImageCount = imageCount, + .imageFormat = surfaceFormat.format, + .imageColorSpace = surfaceFormat.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst, + .preTransform = swapChainSupport.capabilities.currentTransform, + .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, + .presentMode = presentMode, + .clipped = VK_TRUE, + .oldSwapchain = nullptr + }; + + // Find queue families + QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + std::array queueFamilyIndicesLoc = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + // Set sharing mode + if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = vk::SharingMode::eConcurrent; + createInfo.queueFamilyIndexCount = static_cast(queueFamilyIndicesLoc.size()); + createInfo.pQueueFamilyIndices = queueFamilyIndicesLoc.data(); + } else { + createInfo.imageSharingMode = vk::SharingMode::eExclusive; + createInfo.queueFamilyIndexCount = 0; + createInfo.pQueueFamilyIndices = nullptr; + } + + // Create swap chain + swapChain = vk::raii::SwapchainKHR(device, createInfo); + + // Get swap chain images + swapChainImages = swapChain.getImages(); + + // Swapchain images start in UNDEFINED layout; track per-image layout for correct barriers. + swapChainImageLayouts.assign(swapChainImages.size(), vk::ImageLayout::eUndefined); + + // Store swap chain format and extent + swapChainImageFormat = surfaceFormat.format; + swapChainExtent = extent; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create swap chain: " << e.what() << std::endl; + return false; + } +} + +// ===================== Planar reflections resources ===================== +bool Renderer::createReflectionResources(uint32_t width, uint32_t height) { + try { + destroyReflectionResources(); + reflections.clear(); + reflections.resize(MAX_FRAMES_IN_FLIGHT); + reflectionVPs.clear(); + reflectionVPs.resize(MAX_FRAMES_IN_FLIGHT, glm::mat4(1.0f)); + sampleReflectionVP = glm::mat4(1.0f); + + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + auto& rt = reflections[i]; + rt.width = width; + rt.height = height; + + // Color RT: use swapchain format to match existing PBR pipeline rendering formats + vk::Format colorFmt = swapChainImageFormat; + auto [colorImg, colorAlloc] = createImagePooled( + width, + height, + colorFmt, + vk::ImageTiling::eOptimal, + // Allow sampling in glass and blitting to swapchain for diagnostics + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eDeviceLocal, + /*mipLevels*/ + 1, + vk::SharingMode::eExclusive, + {}); + rt.color = std::move(colorImg); + rt.colorAlloc = std::move(colorAlloc); + rt.colorView = createImageView(rt.color, colorFmt, vk::ImageAspectFlagBits::eColor, 1); + // Simple sampler for sampling reflection texture (no mips) + vk::SamplerCreateInfo sampInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear, .mipmapMode = vk::SamplerMipmapMode::eNearest, .addressModeU = vk::SamplerAddressMode::eClampToEdge, .addressModeV = vk::SamplerAddressMode::eClampToEdge, .addressModeW = vk::SamplerAddressMode::eClampToEdge, .minLod = 0.0f, .maxLod = 0.0f}; + rt.colorSampler = vk::raii::Sampler(device, sampInfo); + + // Depth RT + vk::Format depthFmt = findDepthFormat(); + auto [depthImg, depthAlloc] = createImagePooled( + width, + height, + depthFmt, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment, + vk::MemoryPropertyFlagBits::eDeviceLocal, + /*mipLevels*/ + 1, + vk::SharingMode::eExclusive, + {}); + rt.depth = std::move(depthImg); + rt.depthAlloc = std::move(depthAlloc); + rt.depthView = createImageView(rt.depth, depthFmt, vk::ImageAspectFlagBits::eDepth, 1); + } + + // One-time initialization: transition all per-frame reflection color images + // from UNDEFINED to SHADER_READ_ONLY_OPTIMAL so that the first frame can + // legally sample the "previous" frame's image. + if (!reflections.empty()) { + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{.commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + cb.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + std::vector barriers; + barriers.reserve(reflections.size()); + for (auto& rt : reflections) { + if (!!*rt.color) { + barriers.push_back(vk::ImageMemoryBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *rt.color, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }); + } + } + if (!barriers.empty()) { + vk::DependencyInfo depInfo{.imageMemoryBarrierCount = static_cast(barriers.size()), .pImageMemoryBarriers = barriers.data()}; + cb.pipelineBarrier2(depInfo); + } + cb.end(); + vk::SubmitInfo submit{.commandBufferCount = 1, .pCommandBuffers = &*cb}; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submit, *fence); + } + vk::Result result = waitForFencesSafe(*fence, VK_TRUE); + if (result != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for reflection resource fence: " << vk::to_string(result) << std::endl; + } + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create reflection resources: " << e.what() << std::endl; + destroyReflectionResources(); + return false; + } +} + +void Renderer::destroyReflectionResources() { + for (auto& rt : reflections) { + rt.colorSampler = vk::raii::Sampler(nullptr); + rt.colorView = vk::raii::ImageView(nullptr); + rt.colorAlloc = nullptr; + rt.color = vk::raii::Image(nullptr); + rt.depthView = vk::raii::ImageView(nullptr); + rt.depthAlloc = nullptr; + rt.depth = vk::raii::Image(nullptr); + rt.width = rt.height = 0; + } +} + +void Renderer::renderReflectionPass(vk::raii::CommandBuffer& cmd, + const glm::vec4& planeWS, + CameraComponent* camera, + const std::vector& jobs) { + if (reflections.empty()) + return; + auto& rt = reflections[currentFrame]; + if (rt.width == 0 || rt.height == 0 || !*rt.colorView || !*rt.depthView) + return; + + // Transition reflection color to COLOR_ATTACHMENT_OPTIMAL (Sync2) + vk::ImageMemoryBarrier2 toColor2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = {}, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead, + .oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *rt.color, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + // Transition reflection depth to DEPTH_STENCIL_ATTACHMENT_OPTIMAL (Sync2) + vk::ImageMemoryBarrier2 toDepth2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = {}, + .dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests, + .dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentWrite | vk::AccessFlagBits2::eDepthStencilAttachmentRead, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eDepthAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *rt.depth, + .subresourceRange = {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1} + }; + std::array preBarriers{toColor2, toDepth2}; + vk::DependencyInfo depInfoToColor{.imageMemoryBarrierCount = static_cast(preBarriers.size()), .pImageMemoryBarriers = preBarriers.data()}; + cmd.pipelineBarrier2(depInfoToColor); + + vk::RenderingAttachmentInfo colorAtt{ + .imageView = *rt.colorView, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + // Clear to black so scene content dominates reflections + .clearValue = vk::ClearValue{vk::ClearColorValue{std::array < float, 4 >{0.0f, 0.0f, 0.0f, 1.0f}}} + }; + vk::RenderingAttachmentInfo depthAtt{ + .imageView = *rt.depthView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = vk::ClearValue{vk::ClearDepthStencilValue{1.0f, 0}} + }; + vk::RenderingInfo rinfo{ + .renderArea = vk::Rect2D({0, 0}, {rt.width, rt.height}), + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAtt, + .pDepthAttachment = &depthAtt + }; + cmd.beginRendering(rinfo); + // Compute mirrored view matrix about planeWS (default Y=0 plane) + glm::mat4 reflectM(1.0f); + // For Y=0 plane, reflection is simply flip Y + if (glm::length(glm::vec3(planeWS.x, planeWS.y, planeWS.z)) > 0.5f && fabsf(planeWS.y - 1.0f) < 1e-3f && fabsf(planeWS.x) < 1e-3f && fabsf(planeWS.z) < 1e-3f) { + reflectM[1][1] = -1.0f; + } else { + // General plane reflection matrix R = I - 2*n*n^T for normalized plane; ignore translation for now + glm::vec3 n = glm::normalize(glm::vec3(planeWS)); + glm::mat3 R = glm::mat3(1.0f) - 2.0f * glm::outerProduct(n, n); + reflectM = glm::mat4(R); + } + + glm::mat4 viewReflected = camera ? (camera->GetViewMatrix() * reflectM) : reflectM; + glm::mat4 projReflected = camera ? camera->GetProjectionMatrix() : glm::mat4(1.0f); + currentReflectionVP = projReflected * viewReflected; + currentReflectionPlane = planeWS; + if (currentFrame < reflectionVPs.size()) { + reflectionVPs[currentFrame] = currentReflectionVP; + } + + // Set viewport/scissor to reflection RT size + vk::Viewport rv(0.0f, 0.0f, static_cast(rt.width), static_cast(rt.height), 0.0f, 1.0f); + cmd.setViewport(0, rv); + vk::Rect2D rs({0, 0}, {rt.width, rt.height}); + cmd.setScissor(0, rs); + + // Draw opaque entities with mirrored view + // Use reflection-specific pipeline (cull none) to avoid mirrored winding issues. + if (!!*pbrReflectionGraphicsPipeline) { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrReflectionGraphicsPipeline); + } else if (!!*pbrGraphicsPipeline) { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrGraphicsPipeline); + } + + // Prepare frustum for mirrored view to allow culling + FrustumPlanes reflectFrustum = extractFrustumPlanes(currentReflectionVP); + + // Render all jobs (skip transparency) + for (const auto& job : jobs) { + Entity* entity = job.entity; + MeshComponent* meshComponent = job.meshComp; + EntityResources* entityRes = job.entityRes; + MeshResources* meshRes = job.meshRes; + + if (entityRes->cachedIsBlended) + continue; + + // Frustum culling for mirrored view + if (meshComponent->HasLocalAABB()) { + const glm::mat4 model = job.transformComp ? job.transformComp->GetModelMatrix() : glm::mat4(1.0f); + glm::vec3 wmin, wmax; + transformAABB(model, meshComponent->GetLocalAABBMin(), meshComponent->GetLocalAABBMax(), wmin, wmax); + if (!aabbIntersectsFrustum(wmin, wmax, reflectFrustum)) { + continue; // culled from reflection + } + } + + // Bind geometry + std::array buffers = {*meshRes->vertexBuffer, *entityRes->instanceBuffer}; + std::array offsets = {0, 0}; + cmd.bindVertexBuffers(0, buffers, offsets); + cmd.bindIndexBuffer(*meshRes->indexBuffer, 0, vk::IndexType::eUint32); + + // Populate UBO with mirrored view + clip plane and reflection flags + UniformBufferObject ubo{}; + if (job.transformComp) + ubo.model = job.transformComp->GetModelMatrix(); + else + ubo.model = glm::mat4(1.0f); + ubo.view = viewReflected; + ubo.proj = projReflected; + ubo.camPos = glm::vec4(camera ? camera->GetPosition() : glm::vec3(0), 1.0f); + ubo.reflectionPass = 1; + ubo.reflectionEnabled = 0; + ubo.reflectionVP = currentReflectionVP; + ubo.clipPlaneWS = planeWS; + // Ray query shadows in reflection pass + ubo.padding2 = enableRasterRayQueryShadows ? 1.0f : 0.0f; + + updateUniformBufferInternal(currentFrame, entity, entityRes, camera, ubo); + + // Bind descriptor set (PBR set 0) + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *pbrPipelineLayout, + 0, + *entityRes->pbrDescriptorSets[currentFrame], + nullptr); + + // Push material properties + MaterialProperties mp = entityRes->cachedMaterialProps; + // Transmission suppressed during reflection pass via UBO (reflectionPass=1) + mp.transmissionFactor = 0.0f; + pushMaterialProperties(*cmd, mp); + + // Issue draw + uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + cmd.drawIndexed(meshRes->indexCount, instanceCount, 0, 0, 0); + } + + cmd.endRendering(); + + // Transition reflection color to SHADER_READ_ONLY for sampling in main pass (Sync2) + vk::ImageMemoryBarrier2 toSample2{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *rt.color, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depInfoToSample{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &toSample2}; + cmd.pipelineBarrier2(depInfoToSample); +} + +// Create image views +bool Renderer::createImageViews() { + try { + if (xrMode) { + eyeSwapchainImageViews[0].clear(); + eyeSwapchainImageViews[1].clear(); + for (auto& image : eyeSwapchainImages[0]) { + eyeSwapchainImageViews[0].emplace_back(createImageView(image, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1, 2)); // 2 layers for multiview + } + return true; + } + + opaqueSceneColorImages.clear(); + opaqueSceneColorImageAllocations.clear(); + opaqueSceneColorImageViews.clear(); + opaqueSceneColorImageLayouts.clear(); + opaqueSceneColorSampler.clear(); + // Resize image views vector + swapChainImageViews.clear(); + swapChainImageViews.reserve(swapChainImages.size()); + + // Create image view info template (image will be set per iteration) + vk::ImageViewCreateInfo createInfo{ + .viewType = vk::ImageViewType::e2D, + .format = swapChainImageFormat, + .components = { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity + }, + .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1} + }; + + // Create image view for each swap chain image + for (const auto& image : swapChainImages) { + createInfo.image = image; + swapChainImageViews.emplace_back(device, createInfo); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create image views: " << e.what() << std::endl; + return false; + } +} + +// Setup dynamic rendering +bool Renderer::setupDynamicRendering() { + try { + // Create color attachment + colorAttachments = { + vk::RenderingAttachmentInfo{ + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearColorValue(std::array < float, 4 >{0.0f, 0.0f, 0.0f, 1.0f}) + + } + }; + + // Create depth attachment + depthAttachment = vk::RenderingAttachmentInfo{ + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearDepthStencilValue(1.0f, 0) + }; + + // Create rendering info + renderingInfo = vk::RenderingInfo{ + .viewMask = xrMode ? 0x3u : 0x0u, // 0x3 enables views 0 and 1 for multiview + .renderArea = vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent), + .layerCount = 1, + .colorAttachmentCount = static_cast(colorAttachments.size()), + .pColorAttachments = colorAttachments.data(), + .pDepthAttachment = &depthAttachment + }; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to setup dynamic rendering: " << e.what() << std::endl; + return false; + } +} + +// Create command pool +bool Renderer::createCommandPool() { + try { + // Find queue families + QueueFamilyIndices queueFamilyIndicesLoc = findQueueFamilies(physicalDevice); + + // Create command pool info + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndicesLoc.graphicsFamily.value() + }; + + // Create command pool + commandPool = vk::raii::CommandPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create command pool: " << e.what() << std::endl; + return false; + } +} + +// Create command buffers +bool Renderer::createCommandBuffers() { + try { + // Resize command buffers vector + commandBuffers.clear(); + commandBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create command buffer allocation info + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }; + + // Allocate command buffers + commandBuffers = vk::raii::CommandBuffers(device, allocInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create command buffers: " << e.what() << std::endl; + return false; + } +} + +// Create sync objects +bool Renderer::createSyncObjects() { + try { + // Resize semaphores and fences vectors + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + + // Semaphores per swapchain image (indexed by imageIndex from acquireNextImage) + // The presentation engine holds semaphores until the image is re-acquired, so we need + // one semaphore per swapchain image to avoid reuse conflicts. See Vulkan spec: + // https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html + const auto semaphoreCount = static_cast(swapChainImages.size()); + imageAvailableSemaphores.reserve(semaphoreCount); + renderFinishedSemaphores.reserve(semaphoreCount); + + // Fences per frame-in-flight for CPU-GPU synchronization (indexed by currentFrame) + inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create semaphore info + vk::SemaphoreCreateInfo semaphoreInfo{}; + + // Create semaphores per swapchain image (indexed by imageIndex for presentation sync) + for (uint32_t i = 0; i < semaphoreCount; i++) { + imageAvailableSemaphores.emplace_back(device, semaphoreInfo); + renderFinishedSemaphores.emplace_back(device, semaphoreInfo); + } + + // Create fences per frame-in-flight (indexed by currentFrame for CPU-GPU pacing) + vk::FenceCreateInfo fenceInfo{ + .flags = vk::FenceCreateFlagBits::eSignaled + }; + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + inFlightFences.emplace_back(device, fenceInfo); + } + + // Ensure uploads timeline semaphore exists (created early in createLogicalDevice) + // No action needed here unless reinitializing after swapchain recreation. + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create sync objects: " << e.what() << std::endl; + return false; + } +} + +// Clean up swap chain +void Renderer::cleanupSwapChain() { + // Clean up depth resources + depthImageView = vk::raii::ImageView(nullptr); + depthImage = vk::raii::Image(nullptr); + depthImageAllocation = nullptr; + + // Clean up swap chain image views + swapChainImageViews.clear(); + + // Note: Keep descriptor pool alive here to ensure descriptor sets remain valid during swapchain recreation. + // descriptorPool is preserved; it will be managed during full renderer teardown. + + // Destroy reflection render targets if present + destroyReflectionResources(); + + // Clean up pipelines + graphicsPipeline = vk::raii::Pipeline(nullptr); + pbrGraphicsPipeline = vk::raii::Pipeline(nullptr); + lightingPipeline = vk::raii::Pipeline(nullptr); + + // Clean up pipeline layouts + pipelineLayout = vk::raii::PipelineLayout(nullptr); + pbrPipelineLayout = vk::raii::PipelineLayout(nullptr); + lightingPipelineLayout = vk::raii::PipelineLayout(nullptr); + + // Clean up sync objects (they need to be recreated with new swap chain image count) + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + + // Clean up swap chain + swapChain = vk::raii::SwapchainKHR(nullptr); +} + +// Recreate swap chain +void Renderer::recreateSwapChain() { + // Prevent background uploads worker from mutating descriptors while we rebuild + StopUploadsWorker(); + + // Block descriptor writes while we rebuild swapchain and descriptor pools + descriptorSetsValid.store(false, std::memory_order_relaxed); { + // Drop any deferred descriptor updates that target old descriptor sets + std::lock_guard lk(pendingDescMutex); + pendingDescOps.clear(); + descriptorRefreshPending.store(false, std::memory_order_relaxed); + } + + // Wait for all frames in flight to complete before recreating the swap chain + std::vector allFences; + allFences.reserve(inFlightFences.size()); + for (const auto& fence : inFlightFences) { + allFences.push_back(*fence); + } + if (!allFences.empty()) { + vk::Result result = waitForFencesSafe(allFences, VK_TRUE); + if (result != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for in-flight fences during swap chain recreation: " << vk::to_string(result) << std::endl; + } + } + + // Wait for the device to be idle before recreating the swap chain + // External synchronization required (VVL): serialize against queue submits/present. + WaitIdle(); + + // Clean up old swap chain resources + cleanupSwapChain(); + + // Recreate swap chain and related resources + createSwapChain(); + createImageViews(); + setupDynamicRendering(); + createDepthResources(); + + // (Re)create reflection resources if enabled + if (enablePlanarReflections) { + uint32_t rw = std::max(1u, static_cast(static_cast(swapChainExtent.width) * reflectionResolutionScale)); + uint32_t rh = std::max(1u, static_cast(static_cast(swapChainExtent.height) * reflectionResolutionScale)); + createReflectionResources(rw, rh); + } + + // Recreate sync objects with correct sizing for new swap chain + createSyncObjects(); + + // Recreate off-screen opaque scene color and descriptor sets needed by transparent pass + createOpaqueSceneColorResources(); + createTransparentDescriptorSets(); + createTransparentFallbackDescriptorSets(); + + // Wait for all command buffers to complete before clearing resources + for (const auto& fence : inFlightFences) { + vk::Result result = waitForFencesSafe(*fence, VK_TRUE); + if (result != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for fence before clearing resources: " << vk::to_string(result) << std::endl; + } + } + + // Clear all entity descriptor sets since they're now invalid (allocated from the old pool) + { + // Serialize descriptor frees against any other descriptor operations + std::lock_guard lk(descriptorMutex); + for (auto& kv : entityResources) { + auto& resources = kv.second; + resources.basicDescriptorSets.clear(); + resources.pbrDescriptorSets.clear(); + // Descriptor initialization flags must be reset because new descriptor sets + // will be allocated and only the current frame will be initialized at runtime. + resources.pbrUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.basicImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + } + + // Clear ray query descriptor sets - they reference the old output image which will be destroyed + // Must clear before recreating to avoid descriptor set corruption + rayQueryDescriptorSets.clear(); + rayQueryDescriptorsWritten.clear(); + rayQueryDescriptorsDirtyMask.store(0u, std::memory_order_relaxed); + + // Destroy ray query output image resources - they're sized to old swapchain dimensions + rayQueryOutputImageView = vk::raii::ImageView(nullptr); + rayQueryOutputImage = vk::raii::Image(nullptr); + rayQueryOutputImageAllocation = nullptr; + + createGraphicsPipeline(); + createPBRPipeline(); + createLightingPipeline(); + createCompositePipeline(); + + // Recreate Forward+ specific pipelines/resources and resize tile buffers for new extent + if (useForwardPlus) { + createDepthPrepassPipeline(); + uint32_t tilesX = (swapChainExtent.width + forwardPlusTileSizeX - 1) / forwardPlusTileSizeX; + uint32_t tilesY = (swapChainExtent.height + forwardPlusTileSizeY - 1) / forwardPlusTileSizeY; + createOrResizeForwardPlusBuffers(tilesX, tilesY, forwardPlusSlicesZ); + } + + // Re-create command buffers to ensure fresh recording against new swapchain state + commandBuffers.clear(); + createCommandBuffers(); + currentFrame = 0; + + // Recreate ray query resources with new swapchain dimensions + // This must happen after descriptor pool is valid but before marking descriptor sets valid + if (rayQueryEnabled && accelerationStructureEnabled) { + if (!createRayQueryResources()) { + std::cerr << "Warning: Failed to recreate ray query resources after swapchain recreation\n"; + } + } + + // Recreate descriptor sets for all entities after swapchain/pipeline rebuild + for (const auto& kv : entityResources) { + const auto& entity = kv.first; + if (!entity) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + + std::string texturePath = meshComponent->GetTexturePath(); + // Fallback for basic pipeline: use baseColor when legacy path is empty + if (texturePath.empty()) { + const std::string& baseColor = meshComponent->GetBaseColorTexturePath(); + if (!baseColor.empty()) { + texturePath = baseColor; + } + } + // Recreate basic descriptor sets (ignore failures here to avoid breaking resize) + createDescriptorSets(entity, texturePath, false); + // Recreate PBR descriptor sets + createDescriptorSets(entity, texturePath, true); + } + + // Descriptor sets are now valid again + descriptorSetsValid.store(true, std::memory_order_relaxed); + + // Resume background uploads worker now that swapchain and descriptors are recreated + StartUploadsWorker(); +} + +void Renderer::prepareFrameUboTemplate(CameraComponent* camera) { + frameUboTemplate = UniformBufferObject{}; + if (!camera) return; + + frameUboTemplate.view = camera->GetViewMatrix(); + frameUboTemplate.proj = camera->GetProjectionMatrix(); + frameUboTemplate.proj[1][1] *= -1; // Flip Y for Vulkan + frameUboTemplate.camPos = glm::vec4(camera->GetPosition(), 1.0f); + + frameUboTemplate.lightCount = static_cast(lastFrameLightCount); + frameUboTemplate.exposure = std::clamp(this->exposure, 0.2f, 4.0f); + frameUboTemplate.gamma = this->gamma; + frameUboTemplate.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); + frameUboTemplate.nearZ = camera->GetNearPlane(); + frameUboTemplate.farZ = camera->GetFarPlane(); + frameUboTemplate.slicesZ = static_cast(forwardPlusSlicesZ); + + int outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || + swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; + frameUboTemplate.padding0 = outputIsSRGB; + // Raster PBR shader uses padding1 as the Forward+ enable flag. + // 0 = disabled (always use global light loop), non-zero = enabled (use culled tile lists). + frameUboTemplate.padding1 = useForwardPlus ? 1.0f : 0.0f; + frameUboTemplate.padding2 = enableRasterRayQueryShadows ? 1.0f : 0.0f; + + bool reflReady = false; + if (enablePlanarReflections && !reflections.empty()) { + const uint32_t count = static_cast(reflections.size()); + const uint32_t prev = (currentFrame + count - 1u) % count; + auto& rtPrev = reflections[prev]; + reflReady = (!!*rtPrev.colorView) && (!!*rtPrev.colorSampler); + } + frameUboTemplate.reflectionEnabled = reflReady ? 1 : 0; + frameUboTemplate.reflectionVP = sampleReflectionVP; + frameUboTemplate.clipPlaneWS = currentReflectionPlane; + frameUboTemplate.reflectionIntensity = std::clamp(reflectionIntensity, 0.0f, 2.0f); + frameUboTemplate.enableRayQueryReflections = enableRayQueryReflections ? 1 : 0; + frameUboTemplate.enableRayQueryTransparency = enableRayQueryTransparency ? 1 : 0; + + // Ray-query shared buffers are also used by raster PBR when doing ray-query shadows. + // Populate counts so shaders can bounds-check even when running in raster mode. + frameUboTemplate.geometryInfoCount = static_cast(geometryInfoCountCPU); + frameUboTemplate.materialCount = static_cast(materialCountCPU); +} + +// Update uniform buffer +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, EntityResources* entityRes, CameraComponent* camera, TransformComponent* tc) { + if (!entityRes) { + return; + } + + // Get transform component + auto transformComponent = tc ? tc : (entity ? entity->GetComponent() : nullptr); + if (!transformComponent) { + return; + } + + // Create uniform buffer object + UniformBufferObject ubo{}; + ubo.model = transformComponent->GetModelMatrix(); + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.proj[1][1] *= -1; // Flip Y for Vulkan + + // Continue with the rest of the uniform buffer setup + updateUniformBufferInternal(currentImage, entity, entityRes, camera, ubo); +} + +// Overloaded version that accepts a custom transform matrix +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, EntityResources* entityRes, CameraComponent* camera, const glm::mat4& customTransform) { + if (!entityRes) return; + // Create the uniform buffer object with custom transform + UniformBufferObject ubo{}; + ubo.model = customTransform; + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.proj[1][1] *= -1; // Flip Y for Vulkan + + // Continue with the rest of the uniform buffer setup + updateUniformBufferInternal(currentImage, entity, entityRes, camera, ubo); +} + +// Internal helper function to complete uniform buffer setup +void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity, EntityResources* entityRes, CameraComponent* camera, UniformBufferObject& ubo) { + if (!entityRes) { + return; + } + + // Use frame template for most fields + UniformBufferObject finalUbo = frameUboTemplate; + finalUbo.model = ubo.model; + + // For reflection pass, we must override view/proj/reflection flags + if (ubo.reflectionPass == 1) { + finalUbo.views[0] = ubo.views[0]; + finalUbo.projs[0] = ubo.projs[0]; + finalUbo.viewProjections[0] = ubo.projs[0] * ubo.views[0]; + finalUbo.reflectionPass = 1; + finalUbo.reflectionEnabled = 0; + finalUbo.reflectionVP = ubo.reflectionVP; + finalUbo.clipPlaneWS = ubo.clipPlaneWS; + finalUbo.padding2 = ubo.padding2; + } + + // Copy to uniform buffer (guard against null mapped pointer) + void* dst = entityRes->uniformBuffersMapped[currentImage]; + if (!dst) { + std::cerr << "Warning: UBO mapped ptr null for entity '" << (entity ? entity->GetName() : "unknown") << "' frame " << currentImage << std::endl; + return; + } + std::memcpy(dst, &finalUbo, sizeof(UniformBufferObject)); +} + +void Renderer::drawRenderJob(const vk::raii::CommandBuffer& cmd, const RenderJob& job, uint32_t currentFrame, uint32_t eye, bool transparent) { + EntityResources* entityRes = job.entityRes; + MeshResources* meshRes = job.meshRes; + MeshComponent* meshComponent = job.meshComp; + + // 1. Determine descriptor set index + // In multiview, we only need one set per frame as both views share the same UBO + uint32_t setIndex = currentFrame; + + // 2. Update UBO using the current template (which has correct multiview matrices if in XR) + updateUniformBufferInternal(setIndex, job.entity, entityRes, nullptr, frameUboTemplate); + + // 3. Bind geometry + std::array buffers = {*meshRes->vertexBuffer, *entityRes->instanceBuffer}; + std::array offsets = {0, 0}; + cmd.bindVertexBuffers(0, buffers, offsets); + cmd.bindIndexBuffer(*meshRes->indexBuffer, 0, vk::IndexType::eUint32); + + // 4. Bind descriptor set + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *pbrPipelineLayout, + 0, + *entityRes->pbrDescriptorSets[setIndex], + nullptr); + + // 5. Push material properties + pushMaterialProperties(*cmd, entityRes->cachedMaterialProps); + + // 6. Issue draw + uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + cmd.drawIndexed(meshRes->indexCount, instanceCount, 0, 0, 0); +} + +void Renderer::ensureEntityMaterialCache(Entity* entity, EntityResources& res) { + if (!entity) + return; + + if (res.materialCacheValid) + return; + + res.materialCacheValid = true; + res.cachedMaterial = nullptr; + res.cachedIsBlended = false; + res.cachedIsGlass = false; + res.cachedIsLiquid = false; + + // Defaults represent the common case (no explicit material); textures come from descriptor bindings. + MaterialProperties mp{}; + // Sensible defaults for entities without explicit material + mp.baseColorFactor = glm::vec4(1.0f); + mp.metallicFactor = 0.0f; + mp.roughnessFactor = 1.0f; + mp.baseColorTextureSet = 0; + mp.physicalDescriptorTextureSet = 0; + mp.normalTextureSet = -1; + mp.occlusionTextureSet = -1; + mp.emissiveTextureSet = -1; + mp.alphaMask = 0.0f; + mp.alphaMaskCutoff = 0.5f; + mp.emissiveFactor = glm::vec3(0.0f); + mp.emissiveStrength = 1.0f; + mp.transmissionFactor = 0.0f; + mp.useSpecGlossWorkflow = 0; + mp.glossinessFactor = 0.0f; + mp.specularFactor = glm::vec3(1.0f); + mp.ior = 1.5f; + mp.hasEmissiveStrengthExtension = 0; + + if (modelLoader) { + const std::string& entityName = entity->GetName(); + const size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + const size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) { + // Entity name format: "modelName_Material__" + const std::string remainder = entityName.substr(afterTag); + const size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { + const std::string materialName = remainder.substr(nextUnderscore + 1); + if (const Material* material = modelLoader->GetMaterial(materialName)) { + res.cachedMaterial = material; + res.cachedIsGlass = material->isGlass; + res.cachedIsLiquid = material->isLiquid; + + // Base factors + mp.baseColorFactor = glm::vec4(material->albedo, material->alpha); + mp.metallicFactor = material->metallic; + mp.roughnessFactor = material->roughness; + + // Texture set flags (-1 = no texture) + mp.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; + // physical descriptor: MR or SpecGloss + if (material->useSpecularGlossiness) { + mp.useSpecGlossWorkflow = 1; + mp.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + mp.glossinessFactor = material->glossinessFactor; + mp.specularFactor = material->specularFactor; + } else { + mp.useSpecGlossWorkflow = 0; + mp.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + mp.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; + mp.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; + mp.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; + + // Emissive and transmission/IOR + mp.emissiveFactor = material->emissive; + mp.emissiveStrength = material->emissiveStrength; + // Heuristic: consider emissive strength extension present when strength != 1.0 + mp.hasEmissiveStrengthExtension = (std::abs(material->emissiveStrength - 1.0f) > 1e-6f) ? 1 : 0; + mp.transmissionFactor = material->transmissionFactor; + mp.ior = material->ior; + + // Alpha mask handling + mp.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; + mp.alphaMaskCutoff = material->alphaCutoff; + + // Blended classification (opaque materials stay in the opaque pass) + const bool alphaBlend = (material->alphaMode == "BLEND"); + const bool highTransmission = (material->transmissionFactor > 0.2f); + res.cachedIsBlended = alphaBlend || highTransmission || res.cachedIsGlass || res.cachedIsLiquid; + } + } + } + } + } + + res.cachedMaterialProps = mp; +} + +// Render the scene (unique_ptr container overload) +// Convert to a raw-pointer snapshot so callers can safely release their container locks. +void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { + std::vector snapshot; + snapshot.reserve(entities.size()); + for (const auto& uptr : entities) { + snapshot.push_back(uptr.get()); + } + Render(snapshot, camera, imguiSystem); +} + +// Render the scene (raw pointer snapshot overload) +void Renderer::Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem, XrTime predictedTime) { + // Update watchdog timestamp to prove frame is progressing + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + watchdogProgressLabel.store("RenderXR: frame begin", std::memory_order_relaxed); + + if (memoryPool) + memoryPool->setRenderingActive(true); + + // Prepare frame-constant UBO data once for both eyes + // We'll update the eye-specific view/proj matrices inside the eye loop + prepareFrameUboTemplate(camera); + + // Wait for the previous frame's work on this frame slot to complete + vk::Result fenceResult = waitForFencesSafe(*inFlightFences[currentFrame], VK_TRUE); + if (fenceResult != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for in-flight fence (XR): " << vk::to_string(fenceResult) << std::endl; + } + device.resetFences(*inFlightFences[currentFrame]); + + // Execute any pending GPU uploads/preallocations + ProcessPendingMeshUploads(); + ProcessPendingEntityPreallocations(); + + // Preparation pass: Culling and classification + // In a real XR engine, we might do this per eye, but for a simple engine, + // we can use a combined frustum or just render everything for both eyes. + std::vector opaqueJobs; + std::vector transparentJobs; + // ... Simplified: just use all active entities for both eyes for now ... + for (Entity* entity : entities) { + if (!entity || !entity->IsActive()) continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; + + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) continue; + + EntityResources& entityRes = entityIt->second; + ensureEntityMaterialCache(entity, entityRes); + + RenderJob job{entity, &entityRes, &meshResources[meshComponent], 0.0f}; + if (entityRes.cachedIsBlended) { + transparentJobs.push_back(job); + } else { + opaqueJobs.push_back(job); + } + } + + // --- Start Command Buffer --- + vk::raii::CommandBuffer& cmd = commandBuffers[currentFrame]; + cmd.reset(); + cmd.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + // 1. Acquire and Wait for OpenXR swapchain image + uint32_t imageIndex = xrContext.acquireSwapchainImage(); + xrContext.waitSwapchainImage(); + + vk::Image swapchainImage = eyeSwapchainImages[0][imageIndex]; + vk::ImageView swapchainView = *eyeSwapchainImageViews[0][imageIndex]; + + // 2. Transition image to COLOR_ATTACHMENT_OPTIMAL + transitionImageLayout(cmd, swapchainImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}); // 2 layers + + // 3. Prepare Multiview UBO Template + for (uint32_t eye = 0; eye < 2; ++eye) { + glm::mat4 view = camera ? camera->GetViewMatrix(eye) : glm::mat4(1.0f); + glm::mat4 proj = camera ? camera->GetProjectionMatrix(eye) : glm::mat4(1.0f); + proj[1][1] *= -1.0f; // Vulkan Y-flip + frameUboTemplate.views[eye] = view; + frameUboTemplate.projs[eye] = proj; + frameUboTemplate.viewProjections[eye] = proj * view; + frameUboTemplate.camPoses[eye] = glm::vec4(xrContext.getEyePosition(eye), 1.0f); + } + + // 4. Begin Rendering (Single-Pass Multiview) + vk::RenderingAttachmentInfo colorAtt{ + .imageView = swapchainView, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.1f, 0.1f, 0.1f, 1.0f}}} + }; + + // Note: depth buffer must be 2 layers for multiview + vk::RenderingAttachmentInfo depthAtt{ + .imageView = *depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = vk::ClearValue{vk::ClearDepthStencilValue{1.0f, 0}} + }; + + vk::RenderingInfo rinfo{ + .viewMask = 0x3u, // Enable view 0 and 1 + .renderArea = vk::Rect2D({0, 0}, xrContext.getSwapchainExtent()), + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAtt, + .pDepthAttachment = &depthAtt + }; + + cmd.beginRendering(rinfo); + + // Set Viewport and Scissor (shared for both eyes in multiview) + cmd.setViewport(0, xrContext.getViewport(0)); + cmd.setScissor(0, xrContext.getScissor(0)); + + // Draw opaque objects + for (const auto& job : opaqueJobs) { + drawRenderJob(cmd, job, currentFrame, 0, false); + } + + // Draw transparent objects + for (const auto& job : transparentJobs) { + drawRenderJob(cmd, job, currentFrame, 0, true); + } + + cmd.endRendering(); + + // 5. Transition back (OpenXR often expects this or TransferSrcOptimal for blitting) + transitionImageLayout(cmd, swapchainImage, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}); + + // 6. Release OpenXR swapchain image + xrContext.releaseSwapchainImage(); + + cmd.end(); + + // --- Submit to Graphics Queue --- + vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; + vk::SubmitInfo submitInfo{ + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .pWaitDstStageMask = waitStages, + .commandBufferCount = 1, + .pCommandBuffers = &*cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr + }; + + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + } + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} + +void Renderer::Render(const std::vector& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { + // Update watchdog timestamp to prove frame is progressing + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + watchdogProgressLabel.store("Render: frame begin", std::memory_order_relaxed); + + if (memoryPool) + memoryPool->setRenderingActive(true); + struct RenderingStateGuard { + MemoryPool* pool; + explicit RenderingStateGuard(MemoryPool* p) : pool(p) { + } + ~RenderingStateGuard() { + if (pool) + pool->setRenderingActive(false); + } + } guard(memoryPool.get()); + + // Track if ray query rendered successfully this frame to skip rasterization code path + bool rayQueryRenderedThisFrame = false; + + // --- Extract lights for the frame --- + // Build a single light list once per frame (emissive lights only for this scene) + std::vector lightsSubset; + if (!staticLights.empty()) { + lightsSubset.reserve(std::min(staticLights.size(), static_cast(MAX_ACTIVE_LIGHTS))); + for (const auto& L : staticLights) { + // Include all lights (Directional, Point, Emissive) up to the limit + lightsSubset.push_back(L); + if (lightsSubset.size() >= MAX_ACTIVE_LIGHTS) + break; + } + } + lastFrameLightCount = static_cast(lightsSubset.size()); + if (!lightsSubset.empty()) { + updateLightStorageBuffer(currentFrame, lightsSubset, camera); + } + + // Pre-calculate frame-constant UBO data + prepareFrameUboTemplate(camera); + + // Wait for the previous frame's work on this frame slot to complete + // Use a finite timeout loop so we can keep the watchdog alive during long GPU work + // (e.g., acceleration structure builds/refits can legitimately take seconds on large scenes). + watchdogProgressLabel.store("Render: wait inFlightFence", std::memory_order_relaxed); + vk::Result fenceResult = waitForFencesSafe(*inFlightFences[currentFrame], VK_TRUE); + if (fenceResult != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for in-flight fence: " << vk::to_string(fenceResult) << std::endl; + } + + // Reset the fence immediately after successful wait, before any new work + watchdogProgressLabel.store("Render: reset inFlightFence", std::memory_order_relaxed); + device.resetFences(*inFlightFences[currentFrame]); + + // Execute any pending GPU uploads (enqueued by worker/loading threads) on the render thread + // at this safe point to ensure all Vulkan submits happen on a single thread. + // This prevents validation/GPU-AV PostSubmit crashes due to cross-thread queue usage. + watchdogProgressLabel.store("Render: ProcessPendingMeshUploads", std::memory_order_relaxed); + ProcessPendingMeshUploads(); + // Execute any pending per-entity GPU resource preallocation requested by the scene loader. + // This prevents background threads from mutating `entityResources`/`meshResources` concurrently + // with rendering (which can corrupt unordered_map internals and crash). + watchdogProgressLabel.store("Render: ProcessPendingEntityPreallocations", std::memory_order_relaxed); + ProcessPendingEntityPreallocations(); + watchdogProgressLabel.store("Render: after ProcessPendingEntityPreallocations", std::memory_order_relaxed); + + // Process deferred AS deletion queue at safe point (after fence wait) + // Increment frame counters and delete AS structures that are no longer in use + // Wait for MAX_FRAMES_IN_FLIGHT + 1 frames to ensure GPU has finished all work + // (The +1 ensures we've waited through a full cycle of all frame slots) + { + auto it = pendingASDeletions.begin(); + while (it != pendingASDeletions.end()) { + it->framesSinceDestroy++; + if (it->framesSinceDestroy > MAX_FRAMES_IN_FLIGHT) { + // Safe to delete - all frames have finished using these AS structures + it = pendingASDeletions.erase(it); + } else { + ++it; + } + } + } + watchdogProgressLabel.store("Render: after pendingASDeletions", std::memory_order_relaxed); + + // Opportunistically request AS rebuild when more meshes become ready than in the last built AS. + // This makes the TLAS grow as streaming/allocations complete, then settle (no rebuild spam). + // NOTE: This scan can be relatively heavy and is not needed for the default startup path. + // Only run it when opportunistic rebuilds are enabled. + // While loading, allow opportunistic AS rebuild scanning even if the user-facing toggle is off. + // This prevents nondeterministic “missing outdoor props” across app restarts when the first TLAS + // build happens before all entities exist. + if (rayQueryEnabled && accelerationStructureEnabled && (asOpportunisticRebuildEnabled || IsLoading())) { + watchdogProgressLabel.store("Render: AS readiness scan", std::memory_order_relaxed); + size_t readyRenderableCount = 0; + size_t readyUniqueMeshCount = 0; { + auto lastKick = std::chrono::steady_clock::now(); + auto kickWatchdog = [&]() { + auto now = std::chrono::steady_clock::now(); + if (now - lastKick > std::chrono::milliseconds(200)) { + lastFrameUpdateTime.store(now, std::memory_order_relaxed); + lastKick = now; + } + }; + std::map meshToBLASProbe; + for (Entity* e : entities) { + kickWatchdog(); + if (!e || !e->IsActive()) + continue; + // In Ray Query static-only mode, ignore dynamic/animated entities for readiness + if (IsRayQueryStaticOnly()) { + const std::string& nm = e->GetName(); + if (nm.find("_AnimNode_") != std::string::npos) + continue; + if (!nm.empty() && nm.rfind("Ball_", 0) == 0) + continue; + } + auto meshComp = e->GetComponent(); + if (!meshComp) + continue; + try { + auto it = meshResources.find(meshComp); + if (it == meshResources.end()) + continue; + const auto& res = it->second; + // STRICT readiness: uploads must be finished (staging sizes zero) + if (res.vertexBufferSizeBytes != 0 || res.indexBufferSizeBytes != 0) + continue; + if (!*res.vertexBuffer || !*res.indexBuffer) + continue; + if (res.indexCount == 0) + continue; + } catch (...) { + continue; + } + readyRenderableCount++; + if (meshToBLASProbe.find(meshComp) == meshToBLASProbe.end()) { + meshToBLASProbe[meshComp] = static_cast(meshToBLASProbe.size()); + } + } + readyUniqueMeshCount = meshToBLASProbe.size(); + } + // During scene loading/finalization, the TLAS may be built before all entities exist. + // Allow rebuilds even if AS is "frozen" so the TLAS converges to the full scene across restarts. + if ((!asFrozen || IsLoading()) && (readyRenderableCount > lastASBuiltInstanceCount || readyUniqueMeshCount > lastASBuiltBLASCount) && !asBuildRequested.load(std::memory_order_relaxed)) { + std::cout << "AS rebuild requested: counts increased (built instances=" << lastASBuiltInstanceCount + << ", ready instances=" << readyRenderableCount + << ", built meshes=" << lastASBuiltBLASCount + << ", ready meshes=" << readyUniqueMeshCount << ")\n"; + RequestAccelerationStructureBuild("counts increased"); + } + + // Post-load repair: if loading is done and the current TLAS instance count is far below readiness, + // force a one-time rebuild even when frozen so we include the whole scene. + if (!IsLoading() && !asBuildRequested.load(std::memory_order_relaxed)) { + const size_t targetInstances = readyRenderableCount; + if (targetInstances > 0 && lastASBuiltInstanceCount < static_cast(static_cast(targetInstances) * 0.95)) { + asDevOverrideAllowRebuild = true; // allow rebuild even if frozen + std::cout << "AS rebuild requested: post-load full build (built instances=" << lastASBuiltInstanceCount + << ", ready instances=" << targetInstances << ")\n"; + RequestAccelerationStructureBuild("post-load full build"); + } + } + } + + // If in Ray Query static-only mode and TLAS not yet built post-load, request a one-time build now. + // (Does not require a readiness scan.) + if (rayQueryEnabled&& accelerationStructureEnabled && currentRenderMode + == + RenderMode::RayQuery&& IsRayQueryStaticOnly() && + !IsLoading() && + !*tlasStructure.handle && !asBuildRequested.load(std::memory_order_relaxed) + ) { + RequestAccelerationStructureBuild("static-only initial build"); + } + + // Check if acceleration structure build was requested (e.g., after scene loading or counts grew) + // Build at this safe frame point to avoid threading issues + watchdogProgressLabel.store("Render: AS build request check", std::memory_order_relaxed); + if (asBuildRequested.load(std::memory_order_acquire)) { + watchdogProgressLabel.store("Render: AS build request handling", std::memory_order_relaxed); + + // Defer TLAS/BLAS build while the scene loader is still active to avoid partial builds. + // IMPORTANT: Do NOT use IsLoading() here; IsLoading() also includes the post-load + // "finalizing" stage, and deferring on that would deadlock the AS build forever. + if (IsSceneLoaderActive()) { + // Keep the request flag set; we'll build once the loader (and critical textures) finish. + } else if (asFrozen && !asDevOverrideAllowRebuild && !IsLoading()) { + // Ignore rebuilds while frozen to avoid wiping TLAS during animation playback + std::cout << "AS rebuild request ignored (frozen). Reason: " << lastASBuildRequestReason << "\n"; + asBuildRequested.store(false, std::memory_order_release); + asBuildRequestStartNs.store(0, std::memory_order_relaxed); + watchdogSuppressed.store(false, std::memory_order_relaxed); + } else { + // Gate initial build until readiness is high enough to represent the full scene + size_t totalRenderableEntities = 0; + size_t readyRenderableCount = 0; + size_t readyUniqueMeshCount = 0; + size_t missingMeshResources = 0; + size_t pendingUploadsCount = 0; + size_t nullBuffersCount = 0; + size_t zeroIndicesCount = 0; { + auto lastKick = std::chrono::steady_clock::now(); + auto kickWatchdog = [&]() { + auto now = std::chrono::steady_clock::now(); + if (now - lastKick > std::chrono::milliseconds(200)) { + lastFrameUpdateTime.store(now, std::memory_order_relaxed); + lastKick = now; + } + }; + std::map meshToBLASProbe; + for (Entity* e : entities) { + kickWatchdog(); + if (!e || !e->IsActive()) + continue; + // In Ray Query static-only mode, ignore dynamic/animated entities for totals/readiness + if (IsRayQueryStaticOnly()) { + const std::string& nm = e->GetName(); + if (nm.find("_AnimNode_") != std::string::npos) + continue; + if (!nm.empty() && nm.rfind("Ball_", 0) == 0) + continue; + } + auto meshComp = e->GetComponent(); + if (!meshComp) + continue; + totalRenderableEntities++; + try { + auto it = meshResources.find(meshComp); + if (it == meshResources.end()) { + missingMeshResources++; + continue; + } + const auto& res = it->second; + // STRICT readiness here too: uploads finished + if (res.vertexBufferSizeBytes != 0 || res.indexBufferSizeBytes != 0) { + pendingUploadsCount++; + continue; + } + if (!*res.vertexBuffer || !*res.indexBuffer) { + nullBuffersCount++; + continue; + } + if (res.indexCount == 0) { + zeroIndicesCount++; + continue; + } + } catch (...) { + continue; + } + readyRenderableCount++; + if (meshToBLASProbe.find(meshComp) == meshToBLASProbe.end()) { + meshToBLASProbe[meshComp] = static_cast(meshToBLASProbe.size()); + } + } + readyUniqueMeshCount = meshToBLASProbe.size(); + } + const double readiness = (totalRenderableEntities > 0) ? static_cast(readyRenderableCount) / static_cast(totalRenderableEntities) : 0.0; + const double buildThreshold = 0.95; // prefer building when ~full scene is ready + + // Bounded deferral: avoid getting stuck forever waiting for perfect readiness. + // After a short timeout from the original request, build with the best available data. + const uint64_t reqNs = asBuildRequestStartNs.load(std::memory_order_relaxed); + const uint64_t nowNs = std::chrono::steady_clock::now().time_since_epoch().count(); + const double maxDeferralSeconds = 15.0; + const bool deferralTimedOut = (reqNs != 0) && (nowNs > reqNs) && + (static_cast(nowNs - reqNs) / 1'000'000'000.0) >= maxDeferralSeconds; + + if (readiness < buildThreshold && !asDevOverrideAllowRebuild && !deferralTimedOut) { + // Intentionally no stdout spam here (Windows consoles are slow and there's no user-facing benefit). + // Keep the request flag set; try again next frame + } else { + if (deferralTimedOut && readiness < buildThreshold && !asDevOverrideAllowRebuild) { + std::cout << "AS build forced after " << maxDeferralSeconds + << "s deferral (readiness " << readyRenderableCount << "/" << totalRenderableEntities + << ", uniqueMeshesReady=" << readyUniqueMeshCount << ")\n"; + } + struct WatchdogSuppressGuard { + std::atomic& flag; + explicit WatchdogSuppressGuard(std::atomic& f) : flag(f) { + flag.store(true, std::memory_order_relaxed); + } + ~WatchdogSuppressGuard() { + flag.store(false, std::memory_order_relaxed); + } + } watchdogGuard(watchdogSuppressed); + + // Ensure previous GPU work is complete BEFORE building AS. + // + // Wait for all *other* frame-in-flight fences to signal using a finite timeout loop + // and kick the watchdog while we wait. + // Do NOT include `currentFrame` here because its fence was reset at frame start + // and will not signal until we submit the current frame. + { + std::vector fencesToWait; + if (inFlightFences.size() > 1) { + fencesToWait.reserve(inFlightFences.size() - 1); + } + for (uint32_t i = 0; i < static_cast(inFlightFences.size()); ++i) { + if (i == currentFrame) + continue; + if (!!*inFlightFences[i]) { + fencesToWait.push_back(*inFlightFences[i]); + } + } + if (!fencesToWait.empty()) { + vk::Result result = waitForFencesSafe(fencesToWait, VK_TRUE); + if (result != vk::Result::eSuccess) { + std::cerr << "Error: Failed to wait for fences before acceleration structure build: " << vk::to_string(result) << std::endl; + } + } + } + + watchdogProgressLabel.store("Render: buildAccelerationStructures", std::memory_order_relaxed); + if (buildAccelerationStructures(entities)) { + watchdogProgressLabel.store("Render: after buildAccelerationStructures", std::memory_order_relaxed); + asBuildRequested.store(false, std::memory_order_release); + asBuildRequestStartNs.store(0, std::memory_order_relaxed); + // AS build request resolved; restore normal watchdog sensitivity. + watchdogSuppressed.store(false, std::memory_order_relaxed); + // Transition the loading UI to a finalizing phase (descriptor cold-init, etc.). + if (IsLoading()) { + SetLoadingPhase(LoadingPhase::Finalizing); + SetLoadingPhaseProgress(0.0f); + } + + // The TLAS handle can transition from null -> valid (or change on rebuild). + // Ensure raster PBR descriptor sets (set 0, binding 11 `tlas`) are rewritten after an AS build + // so subsequent Raster draws never see an unwritten/stale acceleration-structure descriptor. + for (auto& kv : entityResources) { + kv.second.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + for (Entity* e : entities) { + MarkEntityDescriptorsDirty(e); + } + + // Freeze only when the built AS covers essentially the full set of renderable entities. + // NOTE: `lastASBuiltInstanceCount` is an ENTITY count; TLAS instance count (instancing) is tracked separately. + if (asFreezeAfterFullBuild) { + const double threshold = 0.95; + if (totalRenderableEntities > 0 && + static_cast(lastASBuiltInstanceCount) >= threshold * static_cast(totalRenderableEntities)) { + asFrozen = true; + } + } + + // One concise TLAS summary with consistent units. + if (!!*tlasStructure.handle) { + if (IsRayQueryStaticOnly()) { + std::cout << "TLAS ready (static-only): tlasInstances=" << lastASBuiltTlasInstanceCount + << ", entities=" << lastASBuiltInstanceCount + << ", BLAS=" << lastASBuiltBLASCount + << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << std::endl; + } else { + std::cout << "TLAS ready: tlasInstances=" << lastASBuiltTlasInstanceCount + << ", entities=" << lastASBuiltInstanceCount + << ", BLAS=" << lastASBuiltBLASCount + << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << std::endl; + } + } + } else { + if (!accelerationStructureEnabled || !rayQueryEnabled) { + // Permanent failure due to lack of support; do not retry. + asBuildRequested.store(false, std::memory_order_release); + asBuildRequestStartNs.store(0, std::memory_order_relaxed); + watchdogSuppressed.store(false, std::memory_order_relaxed); + } else { + // If nothing is ready yet (e.g., mesh uploads still pending), don't spam logs. + if (readyRenderableCount > 0 || readyUniqueMeshCount > 0) { + std::cout << "Failed to build acceleration structures, will retry next frame" << std::endl; + } + } + } + // Reset dev override after one use + asDevOverrideAllowRebuild = false; + } + } + } + + // Safe point: the previous work referencing this frame's descriptor sets is complete. + // Apply any deferred descriptor set updates for entities whose textures finished streaming. + watchdogProgressLabel.store("Render: ProcessDirtyDescriptorsForFrame", std::memory_order_relaxed); + ProcessDirtyDescriptorsForFrame(currentFrame); + watchdogProgressLabel.store("Render: after ProcessDirtyDescriptorsForFrame", std::memory_order_relaxed); + + // --- 1. PREPARATION PASS --- + // Gather active entities with mesh resources, perform per-frame descriptor initialization, + // and execute culling. This single pass replaces multiple redundant scans and reduces map lookups. + std::vector opaqueJobs; + std::vector transparentJobs; + opaqueJobs.reserve(entities.size()); + + { + watchdogProgressLabel.store("Render: preparation pass", std::memory_order_relaxed); + + // Prepare frustum once per frame for culling + FrustumPlanes frustum{}; + const bool doCulling = enableFrustumCulling && camera; + if (doCulling && camera) { + glm::mat4 proj = camera->GetProjectionMatrix(); + proj[1][1] *= -1.0f; + const glm::mat4 vp = proj * camera->GetViewMatrix(); + frustum = extractFrustumPlanes(vp); + } + lastCullingVisibleCount = 0; + lastCullingCulledCount = 0; + + uint32_t entityProcessCount = 0; + for (Entity* entity : entities) { + if (!entity || !entity->IsActive()) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + continue; + + auto meshIt = meshResources.find(meshComponent); + if (meshIt == meshResources.end()) + continue; + + EntityResources& entityRes = entityIt->second; + MeshResources& meshRes = meshIt->second; + + // Ensure material cache is valid once per frame + ensureEntityMaterialCache(entity, entityRes); + + // --- Per-frame Descriptor Cold-Init (Integrated) --- + if (entityRes.basicDescriptorSets.empty() || entityRes.pbrDescriptorSets.empty()) { + std::string texPath = meshComponent->GetBaseColorTexturePath(); + if (texPath.empty()) texPath = meshComponent->GetTexturePath(); + if (entityRes.basicDescriptorSets.empty()) createDescriptorSets(entity, entityRes, texPath, false); + if (entityRes.pbrDescriptorSets.empty()) createDescriptorSets(entity, entityRes, texPath, true); + } + + // Initialize binding 0 (UBO) for the current frame slot if not already done. + if (!entityRes.pbrUboBindingWritten[currentFrame] || !entityRes.basicUboBindingWritten[currentFrame]) { + std::string texPath = meshComponent->GetBaseColorTexturePath(); + if (texPath.empty()) texPath = meshComponent->GetTexturePath(); + if (!entityRes.pbrUboBindingWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, true, currentFrame, false, true); + } + if (!entityRes.basicUboBindingWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, false, currentFrame, false, true); + } + } + + // Initialize images for the current frame slot if not already done. + if (!entityRes.pbrImagesWritten[currentFrame] || !entityRes.basicImagesWritten[currentFrame]) { + std::string texPath = meshComponent->GetBaseColorTexturePath(); + if (texPath.empty()) texPath = meshComponent->GetTexturePath(); + if (!entityRes.pbrImagesWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, true, currentFrame, true, false); + entityRes.pbrImagesWritten[currentFrame] = true; + } + if (!entityRes.basicImagesWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, false, currentFrame, true, false); + entityRes.basicImagesWritten[currentFrame] = true; + } + } + + // --- Culling & Classification --- + auto* tc = entity->GetComponent(); + bool useBlended = entityRes.cachedIsBlended; + + if (meshComponent->HasLocalAABB()) { + const glm::mat4 model = tc ? tc->GetModelMatrix() : glm::mat4(1.0f); + glm::vec3 wmin, wmax; + transformAABB(model, meshComponent->GetLocalAABBMin(), meshComponent->GetLocalAABBMax(), wmin, wmax); + + // 1. Frustum Culling + if (doCulling && !aabbIntersectsFrustum(wmin, wmax, frustum)) { + lastCullingCulledCount++; + continue; + } + + // 2. Distance-based LOD + if (enableDistanceLOD && camera) { + glm::vec3 camPos = camera->GetPosition(); + bool cameraInside = (camPos.x >= wmin.x && camPos.x <= wmax.x && + camPos.y >= wmin.y && camPos.y <= wmax.y && + camPos.z >= wmin.z && camPos.z <= wmax.z); + if (!cameraInside) { + float dx = std::max({0.0f, wmin.x - camPos.x, camPos.x - wmax.x}); + float dy = std::max({0.0f, wmin.y - camPos.y, camPos.y - wmax.y}); + float dz = std::max({0.0f, wmin.z - camPos.z, camPos.z - wmax.z}); + float dist = std::sqrt(dx * dx + dy * dy + dz * dz); + float z_eff = std::max(0.1f, dist); + float fov = glm::radians(camera->GetFieldOfView()); + float radius = glm::length(0.5f * (wmax - wmin)); + float pixelDiameter = (radius * 2.0f * static_cast(swapChainExtent.height)) / (z_eff * 2.0f * std::tan(fov * 0.5f)); + float threshold = useBlended ? lodPixelThresholdTransparent : lodPixelThresholdOpaque; + if (pixelDiameter < threshold) { + lastCullingCulledCount++; + continue; + } + } + } + } + + lastCullingVisibleCount++; + bool isAlphaMasked = false; + if (entityRes.materialCacheValid) { + isAlphaMasked = (entityRes.cachedMaterialProps.alphaMask > 0.5f); + } + + // Update UBO for visible entity once per frame (shared across all main passes) + updateUniformBuffer(currentFrame, entity, &entityRes, camera, tc); + + RenderJob job{entity, &entityRes, &meshRes, meshComponent, tc, isAlphaMasked}; + if (useBlended) { + transparentJobs.push_back(job); + } else { + opaqueJobs.push_back(job); + } + + // Update watchdog periodically + if (++entityProcessCount % 100 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + } + watchdogProgressLabel.store("Render: after preparation pass", std::memory_order_relaxed); + } + + // If the scene loader has finished and there are no remaining blocking tasks, + // hide the fullscreen loading overlay. + if (IsLoading() && GetLoadingPhase() == LoadingPhase::Finalizing) { + const bool loaderDone = !loadingFlag.load(std::memory_order_relaxed); + const bool criticalDone = (criticalJobsOutstanding.load(std::memory_order_relaxed) == 0u); + const bool noASPending = !asBuildRequested.load(std::memory_order_relaxed); + const bool noPreallocPending = !pendingEntityPreallocQueued.load(std::memory_order_relaxed); + const bool noDirtyEntities = descriptorDirtyEntities.empty(); + const bool noDeferredDescOps = !descriptorRefreshPending.load(std::memory_order_relaxed); + if (loaderDone && criticalDone && noASPending && noPreallocPending && noDirtyEntities && noDeferredDescOps) { + MarkInitialLoadComplete(); + } + } + + // Safe point: flush any descriptor updates that were deferred while a command buffer + // was recording in a prior frame. Only apply ops for the current frame to avoid + // update-after-bind on pending frames. + if (descriptorRefreshPending.load(std::memory_order_relaxed)) { + watchdogProgressLabel.store("Render: flush deferred descriptor ops", std::memory_order_relaxed); + std::vector ops; { + std::lock_guard lk(pendingDescMutex); + ops.swap(pendingDescOps); + descriptorRefreshPending.store(false, std::memory_order_relaxed); + } + uint32_t opCount = 0; + for (auto& op : ops) { + // Kick watchdog periodically during potentially heavy descriptor update bursts + if ((++opCount % 50u) == 0u) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + if (op.frameIndex == currentFrame) { + // Now not recording; safe to apply updates for this frame + updateDescriptorSetsForFrame(op.entity, op.texPath, op.usePBR, op.frameIndex, op.imagesOnly); + } else { + // Keep other frame ops queued for next frame’s safe point + std::lock_guard lk(pendingDescMutex); + pendingDescOps.push_back(op); + descriptorRefreshPending.store(true, std::memory_order_relaxed); + } + } + watchdogProgressLabel.store("Render: after deferred descriptor ops", std::memory_order_relaxed); + } + + // Safe point: handle any pending reflection resource (re)creation and per-frame descriptor refreshes + if (reflectionResourcesDirty) { + if (enablePlanarReflections) { + uint32_t rw = std::max(1u, static_cast(static_cast(swapChainExtent.width) * reflectionResolutionScale)); + uint32_t rh = std::max(1u, static_cast(static_cast(swapChainExtent.height) * reflectionResolutionScale)); + createReflectionResources(rw, rh); + } else { + destroyReflectionResources(); + } + reflectionResourcesDirty = false; + } + + // Reflection descriptor binding refresh is handled elsewhere; avoid redundant per-frame mass updates here. + // Pick the VP associated with the previous frame's reflection texture for sampling in the main pass + if (enablePlanarReflections && !reflectionVPs.empty()) { + uint32_t prev = (currentFrame > 0) ? (currentFrame - 1) : (static_cast(reflectionVPs.size()) - 1); + sampleReflectionVP = reflectionVPs[prev]; + } + + // This function updates bindings 6/7/8 (storage buffers) which don't have UPDATE_AFTER_BIND. + // Updating these every frame causes "updated without UPDATE_AFTER_BIND" errors with MAX_FRAMES_IN_FLIGHT > 1. + // These bindings are already initialized in createDescriptorSets and updated when buffers change. + // Binding 10 (reflection map) has UPDATE_AFTER_BIND and can be updated separately if needed. + // refreshPBRForwardPlusBindingsForFrame(currentFrame); + + // Acquire next swapchain image + // acquireNextImage returns imageIndex (which swapchain image is available). + // Use currentFrame to select an imageAvailableSemaphore for acquire. + // Use imageIndex to select renderFinishedSemaphore for present (ties semaphore to the specific image). + const uint32_t acquireSemaphoreIndex = currentFrame % static_cast(imageAvailableSemaphores.size()); + + uint32_t imageIndex; + vk::Result acquireResultCode = vk::Result::eSuccess; + // Helper overloads to normalize acquireNextImage return across Vulkan-Hpp versions + auto extractAcquire = [](auto const& ret, vk::Result& code, uint32_t& idx) { + using RetT = std::decay_t; + if constexpr (std::is_same_v>) { + code = ret.result; + idx = ret.value; + } else { + // Assume older std::pair + code = ret.first; + idx = ret.second; + } + }; + try { + watchdogProgressLabel.store("Render: acquireNextImage", std::memory_order_relaxed); + auto acquireRet = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[acquireSemaphoreIndex]); + // Vulkan-Hpp changed the return type of acquireNextImage for RAII swapchain across versions. + // Support both vk::ResultValue (newer) and std::pair (older). + extractAcquire(acquireRet, acquireResultCode, imageIndex); + } catch (const vk::OutOfDateKHRError&) { + watchdogProgressLabel.store("Render: acquireNextImage out-of-date", std::memory_order_relaxed); + // Swapchain is out of date (e.g., window resized) before we could + // query the result. Trigger recreation and exit this frame cleanly. + framebufferResized.store(true, std::memory_order_relaxed); + if (imguiSystem) + ImGui::EndFrame(); + // IMPORTANT: We already reset the in-flight fence at the start of the frame. + // Because we're exiting early (no submit), signal it via an empty submit so + // swapchain recreation won't hang waiting for an unsignaled fence. + { + vk::SubmitInfo2 emptySubmit2{}; + std::lock_guard lock(queueMutex); + graphicsQueue.submit2(emptySubmit2, *inFlightFences[currentFrame]); + } + recreateSwapChain(); + return; + } + + // imageIndex already populated above + watchdogProgressLabel.store("Render: acquired swapchain image", std::memory_order_relaxed); + + if (acquireResultCode == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { + framebufferResized.store(false, std::memory_order_relaxed); + if (imguiSystem) + ImGui::EndFrame(); + // Fence was reset earlier; ensure it is signaled before we bail out + // to avoid a deadlock in swapchain recreation. + { + vk::SubmitInfo2 emptySubmit2{}; + std::lock_guard lock(queueMutex); + graphicsQueue.submit2(emptySubmit2, *inFlightFences[currentFrame]); + } + recreateSwapChain(); + return; + } + if (acquireResultCode != vk::Result::eSuccess) { + throw std::runtime_error("Failed to acquire swap chain image"); + } + + if (framebufferResized.load(std::memory_order_relaxed)) { + // Signal the fence via empty submit since no real work will be submitted + // this frame, preventing a wait on an unsignaled fence during resize. + { + vk::SubmitInfo2 emptySubmit2{}; + std::lock_guard lock(queueMutex); + graphicsQueue.submit2(emptySubmit2, *inFlightFences[currentFrame]); + } + recreateSwapChain(); + return; + } + + // Perform any descriptor updates that must not happen during command buffer recording + if (useForwardPlus) { + uint32_t tilesX_pre = (swapChainExtent.width + forwardPlusTileSizeX - 1) / forwardPlusTileSizeX; + uint32_t tilesY_pre = (swapChainExtent.height + forwardPlusTileSizeY - 1) / forwardPlusTileSizeY; + // Only update current frame's descriptors to avoid touching in-flight frames + createOrResizeForwardPlusBuffers(tilesX_pre, tilesY_pre, forwardPlusSlicesZ, /*updateOnlyCurrentFrame=*/true); + // After (re)creating Forward+ buffers, bindings 7/8 will be refreshed as needed. + } + + // Ensure light buffers are sufficiently large before recording to avoid resizing while in use + { + // Reserve capacity based on emissive lights only (punctual lights disabled for now) + size_t desiredLightCapacity = 0; + if (!staticLights.empty()) { + size_t emissiveCount = 0; + for (const auto& L : staticLights) { + if (L.type == ExtractedLight::Type::Emissive) { + ++emissiveCount; + if (emissiveCount >= MAX_ACTIVE_LIGHTS) + break; + } + } + desiredLightCapacity = emissiveCount; + } + if (desiredLightCapacity > 0) { + createOrResizeLightStorageBuffers(desiredLightCapacity); + // Ensure compute (binding 0) sees the current frame's lights buffer + refreshForwardPlusComputeLightsBindingForFrame(currentFrame); + // Bindings 6/7/8 for PBR are refreshed only when buffers change (handled in resize path). + } + } + + // Safe point: Update ray query descriptor sets if ray query mode is active + // This MUST happen before command buffer recording starts to avoid "descriptor updated without UPDATE_AFTER_BIND" errors + if (currentRenderMode == RenderMode::RayQuery && rayQueryEnabled && accelerationStructureEnabled) { + if (!!*tlasStructure.handle) { + watchdogProgressLabel.store("Render: updateRayQueryDescriptorSets", std::memory_order_relaxed); + updateRayQueryDescriptorSets(currentFrame, entities); + watchdogProgressLabel.store("Render: after updateRayQueryDescriptorSets", std::memory_order_relaxed); + } + } + + // Refit TLAS if needed (either for Ray Query mode or for Raster shadows) + const bool needTLAS = (currentRenderMode == RenderMode::RayQuery || enableRasterRayQueryShadows) && accelerationStructureEnabled; + if (needTLAS && !!*tlasStructure.handle) { + if (!IsRayQueryStaticOnly()) { + watchdogProgressLabel.store("Render: refitTopLevelAS", std::memory_order_relaxed); + refitTopLevelAS(entities, camera); + } + } + + commandBuffers[currentFrame].reset(); + // Begin command buffer recording for this frame + commandBuffers[currentFrame].begin(vk::CommandBufferBeginInfo()); + isRecordingCmd.store(true, std::memory_order_relaxed); + if (framebufferResized.load(std::memory_order_relaxed)) { + commandBuffers[currentFrame].end(); + recreateSwapChain(); + return; + } + + // Ray query rendering mode dispatch + if (currentRenderMode == RenderMode::RayQuery && rayQueryEnabled && accelerationStructureEnabled) { + // Check if TLAS handle is valid (dereference RAII handle) + if (!*tlasStructure.handle) { + // TLAS not built yet. + // During loading, allow the raster path (and the progress overlay) to render normally + // instead of presenting a diagnostic magenta frame. + if (!IsLoading()) { + // Present a diagnostic frame from the ray-query path to avoid accidentally showing + // rasterized content in RayQuery mode. + // Transition swapchain image from PRESENT to TRANSFER_DST + vk::ImageMemoryBarrier2 swapchainBarrier{}; + swapchainBarrier.srcStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe; + swapchainBarrier.srcAccessMask = vk::AccessFlagBits2::eNone; + swapchainBarrier.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; + swapchainBarrier.dstAccessMask = vk::AccessFlagBits2::eTransferWrite; + swapchainBarrier.oldLayout = (imageIndex < swapChainImageLayouts.size()) ? swapChainImageLayouts[imageIndex] : vk::ImageLayout::eUndefined; + swapchainBarrier.newLayout = vk::ImageLayout::eTransferDstOptimal; + swapchainBarrier.image = swapChainImages[imageIndex]; + swapchainBarrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + swapchainBarrier.subresourceRange.levelCount = 1; + swapchainBarrier.subresourceRange.layerCount = 1; + + vk::DependencyInfo depInfoSwap{}; + depInfoSwap.imageMemoryBarrierCount = 1; + depInfoSwap.pImageMemoryBarriers = &swapchainBarrier; + commandBuffers[currentFrame].pipelineBarrier2(depInfoSwap); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = swapchainBarrier.newLayout; + + // Clear to a distinct magenta diagnostic color + vk::ClearColorValue clearColor{std::array < float, 4 >{1.0f, 0.0f, 1.0f, 1.0f}}; + vk::ImageSubresourceRange clearRange{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; + commandBuffers[currentFrame].clearColorImage(swapChainImages[imageIndex], vk::ImageLayout::eTransferDstOptimal, clearColor, clearRange); + + // Transition back to PRESENT + swapchainBarrier.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; + swapchainBarrier.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; + swapchainBarrier.dstStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe; + swapchainBarrier.dstAccessMask = vk::AccessFlagBits2::eNone; + swapchainBarrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + swapchainBarrier.newLayout = vk::ImageLayout::ePresentSrcKHR; + commandBuffers[currentFrame].pipelineBarrier2(depInfoSwap); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = swapchainBarrier.newLayout; + + rayQueryRenderedThisFrame = true; // Skip raster; ensure we are looking at RQ path only + } + } else { + // TLAS is valid and descriptor sets were already updated at safe point + // Proceed with ray query rendering + // Bind ray query compute pipeline + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eCompute, *rayQueryPipeline); + + // Bind descriptor set + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + *rayQueryPipelineLayout, + 0, + *rayQueryDescriptorSets[currentFrame], + nullptr); + + // This dedicated UBO is separate from entity UBOs and uses a Ray Query-specific layout. + if (rayQueryUniformBuffersMapped.size() > currentFrame && rayQueryUniformBuffersMapped[currentFrame]) { + RayQueryUniformBufferObject ubo{}; + ubo.model = glm::mat4(1.0f); // Identity - not used for ray query + + // Force view matrix update to reflect current camera position + // (the dirty flag isn't automatically set when camera position changes) + camera->ForceViewMatrixUpdate(); + + // Get camera matrices + glm::mat4 camView = camera->GetViewMatrix(); + ubo.view = camView; + ubo.proj = camera->GetProjectionMatrix(); + ubo.proj[1][1] *= -1; // Flip Y for Vulkan + ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); + // Clamp to sane ranges to avoid black output (exposure=0 → 1-exp(0)=0) + ubo.exposure = std::clamp(exposure, 0.2f, 4.0f); + ubo.gamma = std::clamp(gamma, 1.6f, 2.6f); + // Match raster convention: ambient scale factor for simple IBL/ambient term. + // (Raster defaults to ~1.0 in the main pass; keep Ray Query consistent.) + ubo.scaleIBLAmbient = 1.0f; + // Provide the per-frame light count so the ray query shader can iterate lights. + ubo.lightCount = static_cast(lastFrameLightCount); + ubo.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); + ubo.enableRayQueryReflections = enableRayQueryReflections ? 1 : 0; + ubo.enableRayQueryTransparency = enableRayQueryTransparency ? 1 : 0; + // Max secondary bounces (reflection/refraction). Stored in the padding slot to avoid UBO layout churn. + // Shader clamps this value. + ubo._pad0 = rayQueryMaxBounces; + // Thick-glass toggles and tuning + ubo.enableThickGlass = enableThickGlass ? 1 : 0; + ubo.thicknessClamp = thickGlassThicknessClamp; + ubo.absorptionScale = thickGlassAbsorptionScale; + // Ray Query hard shadows (see `shaders/ray_query.slang`) + ubo._pad1 = enableRayQueryShadows ? 1 : 0; + ubo.shadowSampleCount = std::clamp(rayQueryShadowSampleCount, 1, 32); + ubo.shadowSoftness = std::clamp(rayQueryShadowSoftness, 0.0f, 1.0f); + ubo.reflectionIntensity = reflectionIntensity; + // Provide geometry info count for shader-side bounds checking (per-instance) + ubo.geometryInfoCount = static_cast(tlasInstanceCount); + // Provide material buffer count for shader-side bounds checking + ubo.materialCount = static_cast(materialCountCPU); + + // Copy to mapped memory + std::memcpy(rayQueryUniformBuffersMapped[currentFrame], &ubo, sizeof(RayQueryUniformBufferObject)); + } else { + // Keep concise error for visibility + std::cerr << "Ray Query UBO not mapped for frame " << currentFrame << "\n"; + } + + // Dispatch compute shader (8x8 workgroups as defined in shader) + uint32_t workgroupsX = (swapChainExtent.width + 7) / 8; + uint32_t workgroupsY = (swapChainExtent.height + 7) / 8; + commandBuffers[currentFrame].dispatch(workgroupsX, workgroupsY, 1); + + // Barrier: wait for compute shader to finish writing to output image, + // then make it readable by fragment shader for sampling in composite pass + vk::ImageMemoryBarrier2 rqToSample{}; + rqToSample.srcStageMask = vk::PipelineStageFlagBits2::eComputeShader; + rqToSample.srcAccessMask = vk::AccessFlagBits2::eShaderWrite; + rqToSample.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + rqToSample.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + rqToSample.oldLayout = vk::ImageLayout::eGeneral; + rqToSample.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + rqToSample.image = *rayQueryOutputImage; + rqToSample.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + rqToSample.subresourceRange.levelCount = 1; + rqToSample.subresourceRange.layerCount = 1; + + vk::DependencyInfo depRQToSample{}; + depRQToSample.imageMemoryBarrierCount = 1; + depRQToSample.pImageMemoryBarriers = &rqToSample; + commandBuffers[currentFrame].pipelineBarrier2(depRQToSample); + + // Composite fullscreen: sample rayQueryOutputImage to the swapchain using the composite pipeline + // Transition swapchain image to COLOR_ATTACHMENT_OPTIMAL + vk::ImageMemoryBarrier2 swapchainToColor{}; + swapchainToColor.srcStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe; + swapchainToColor.srcAccessMask = vk::AccessFlagBits2::eNone; + swapchainToColor.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; + swapchainToColor.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; + swapchainToColor.oldLayout = (imageIndex < swapChainImageLayouts.size()) ? swapChainImageLayouts[imageIndex] : vk::ImageLayout::eUndefined; + swapchainToColor.newLayout = vk::ImageLayout::eColorAttachmentOptimal; + swapchainToColor.image = swapChainImages[imageIndex]; + swapchainToColor.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + swapchainToColor.subresourceRange.levelCount = 1; + swapchainToColor.subresourceRange.layerCount = 1; + vk::DependencyInfo depSwapToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &swapchainToColor}; + commandBuffers[currentFrame].pipelineBarrier2(depSwapToColor); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = swapchainToColor.newLayout; + + // Begin dynamic rendering for composite (no depth) + colorAttachments[0].imageView = *swapChainImageViews[imageIndex]; + colorAttachments[0].loadOp = vk::AttachmentLoadOp::eClear; + depthAttachment.loadOp = vk::AttachmentLoadOp::eDontCare; + renderingInfo.renderArea = vk::Rect2D({0, 0}, swapChainExtent); + auto savedDepthPtr2 = renderingInfo.pDepthAttachment; + renderingInfo.pDepthAttachment = nullptr; + commandBuffers[currentFrame].beginRendering(renderingInfo); + + if (!!*compositePipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *compositePipeline); + } + vk::Viewport vp(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f); + vk::Rect2D sc({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setViewport(0, vp); + commandBuffers[currentFrame].setScissor(0, sc); + + // Bind the RQ composite descriptor set (samples rayQueryOutputImage) + if (!rqCompositeDescriptorSets.empty()) { + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + *compositePipelineLayout, + 0, + {*rqCompositeDescriptorSets[currentFrame]}, + {}); + } + + // Push exposure/gamma and sRGB flag + struct CompositePush { + float exposure; + float gamma; + int outputIsSRGB; + float _pad; + } pc2{}; + pc2.exposure = std::clamp(this->exposure, 0.2f, 4.0f); + pc2.gamma = this->gamma; + pc2.outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; + commandBuffers[currentFrame].pushConstants(*compositePipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, pc2); + + commandBuffers[currentFrame].draw(3, 1, 0, 0); + commandBuffers[currentFrame].endRendering(); + renderingInfo.pDepthAttachment = savedDepthPtr2; + + // Transition swapchain back to PRESENT and RQ image back to GENERAL for next frame + vk::ImageMemoryBarrier2 swapchainToPresent{}; + swapchainToPresent.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; + swapchainToPresent.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite; + swapchainToPresent.dstStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe; + swapchainToPresent.dstAccessMask = vk::AccessFlagBits2::eNone; + swapchainToPresent.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; + swapchainToPresent.newLayout = vk::ImageLayout::ePresentSrcKHR; + swapchainToPresent.image = swapChainImages[imageIndex]; + swapchainToPresent.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + swapchainToPresent.subresourceRange.levelCount = 1; + swapchainToPresent.subresourceRange.layerCount = 1; + + vk::ImageMemoryBarrier2 rqBackToGeneral{}; + rqBackToGeneral.srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + rqBackToGeneral.srcAccessMask = vk::AccessFlagBits2::eShaderRead; + rqBackToGeneral.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader; + rqBackToGeneral.dstAccessMask = vk::AccessFlagBits2::eShaderWrite; + rqBackToGeneral.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + rqBackToGeneral.newLayout = vk::ImageLayout::eGeneral; + rqBackToGeneral.image = *rayQueryOutputImage; + rqBackToGeneral.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + rqBackToGeneral.subresourceRange.levelCount = 1; + rqBackToGeneral.subresourceRange.layerCount = 1; + + std::array barriers{swapchainToPresent, rqBackToGeneral}; + vk::DependencyInfo depEnd{.imageMemoryBarrierCount = static_cast(barriers.size()), .pImageMemoryBarriers = barriers.data()}; + commandBuffers[currentFrame].pipelineBarrier2(depEnd); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = swapchainToPresent.newLayout; + + // Ray query rendering complete - set flag to skip rasterization code path + rayQueryRenderedThisFrame = true; + } + } + + // Process texture streaming uploads (see Renderer::ProcessPendingTextureJobs) + + vk::raii::Pipeline* currentPipeline = nullptr; + vk::raii::PipelineLayout* currentLayout = nullptr; + + // Incrementally process pending texture uploads on the main thread so that + // all Vulkan submits happen from a single place while worker threads only + // handle CPU-side decoding. While the loading screen is up, prioritize + // critical textures so the first rendered frame looks mostly correct. + if (IsLoading()) { + // Larger budget while loading screen is visible so we don't stall + // streaming of near-field baseColor textures. + ProcessPendingTextureJobs(/*maxJobs=*/16, /*includeCritical=*/true, /*includeNonCritical=*/false); + } else { + // After loading screen disappears, we want the scene to remain + // responsive (~20 fps) while textures stream in. Limit the number + // of non-critical uploads per frame so we don't tank frame time. + static uint32_t streamingFrameCounter = 0; + streamingFrameCounter++; + // Ray Query needs textures visible quickly; process more streaming work when in Ray Query mode. + if (currentRenderMode == RenderMode::RayQuery) { + // Aggressively drain both critical and non-critical queues each frame for faster bring-up. + ProcessPendingTextureJobs(/*maxJobs=*/32, /*includeCritical=*/true, /*includeNonCritical=*/true); + } else { + // Raster path: keep previous throttling to avoid stalls. + if ((streamingFrameCounter % 3) == 0) { + ProcessPendingTextureJobs(/*maxJobs=*/1, /*includeCritical=*/false, /*includeNonCritical=*/true); + } + } + } + + // Renderer UI - available for both ray query and rasterization modes. + // Hide UI during loading; the progress overlay is handled by ImGuiSystem::NewFrame(). + if (imguiSystem && !imguiSystem->IsFrameRendered() && !IsLoading()) { + if (ImGui::Begin("Renderer")) { + // Declare variables that need to persist across conditional blocks + bool prevFwdPlus = useForwardPlus; + + // === RENDERING MODE SELECTION (TOP) === + ImGui::Text("Rendering Mode:"); + if (rayQueryEnabled && accelerationStructureEnabled) { + const char* modeNames[] = {"Rasterization", "Ray Query"}; + int currentMode = (currentRenderMode == RenderMode::RayQuery) ? 1 : 0; + if (ImGui::Combo("Mode", ¤tMode, modeNames, 2)) { + RenderMode newMode = (currentMode == 1) ? RenderMode::RayQuery : RenderMode::Rasterization; + if (newMode != currentRenderMode) { + currentRenderMode = newMode; + std::cout << "Switched to " << modeNames[currentMode] << " mode\n"; + + // Request acceleration structure build when switching to ray query mode + if (currentRenderMode == RenderMode::RayQuery) { + std::cout << "Requesting acceleration structure build...\n"; + RequestAccelerationStructureBuild(); + } + + // Switching modes can change which pipelines are bound and whether ray-query-dependent + // descriptor bindings (e.g., PBR binding 11 `tlas`) become statically used. + // Mark entity descriptor sets dirty so the next safe point refreshes bindings for this frame. + for (auto& kv : entityResources) { + kv.second.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + for (Entity* e : entities) { + MarkEntityDescriptorsDirty(e); + } + } + } + } else { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Rasterization only (ray query not supported)"); + } + + // === RASTERIZATION-SPECIFIC OPTIONS === + if (currentRenderMode == RenderMode::Rasterization) { + ImGui::Separator(); + ImGui::Text("Rasterization Options:"); + + // Lighting Controls - BRDF/PBR is now the default lighting model + bool useBasicLighting = imguiSystem && !imguiSystem->IsPBREnabled(); + if (ImGui::Checkbox("Use Basic Lighting (Phong)", &useBasicLighting)) { + imguiSystem->SetPBREnabled(!useBasicLighting); + std::cout << "Lighting mode: " << (!useBasicLighting ? "BRDF/PBR (default)" : "Basic Phong") << std::endl; + } + + if (!useBasicLighting) { + ImGui::Text("Status: BRDF/PBR pipeline active (default)"); + ImGui::Text("All models rendered with physically-based lighting"); + } else { + ImGui::Text("Status: Basic Phong pipeline active"); + ImGui::Text("All models rendered with basic Phong shading"); + } + + ImGui::Checkbox("Forward+ (tiled light culling)", &useForwardPlus); + if (useForwardPlus && !prevFwdPlus) { + // Lazily create Forward+ resources if enabled at runtime + if (!*forwardPlusPipeline || !*forwardPlusDescriptorSetLayout || forwardPlusPerFrame.empty()) { + createForwardPlusPipelinesAndResources(); + } + if (!*depthPrepassPipeline) { + createDepthPrepassPipeline(); + } + } + + // Raster shadows via ray queries (experimental) + if (rayQueryEnabled && accelerationStructureEnabled) { + ImGui::Checkbox("RayQuery shadows (raster)", &enableRasterRayQueryShadows); + } else { + ImGui::TextDisabled("RayQuery shadows (raster) (requires ray query + AS)"); + } + + // Planar reflections controls + ImGui::Spacing(); + /* + if (ImGui::Checkbox("Planar reflections (experimental)", &enablePlanarReflections)) { + // Defer actual (re)creation/destruction to the next safe point at frame start + reflectionResourcesDirty = true; + } + */ + enablePlanarReflections = false; + float scaleBefore = reflectionResolutionScale; + if (ImGui::SliderFloat("Reflection resolution scale", &reflectionResolutionScale, 0.25f, 1.0f, "%.2f")) { + reflectionResolutionScale = std::clamp(reflectionResolutionScale, 0.25f, 1.0f); + if (enablePlanarReflections&& std::abs(scaleBefore - reflectionResolutionScale) + > + 1e-3f + ) { + reflectionResourcesDirty = true; + } + } + if (enablePlanarReflections && !reflections.empty()) { + auto& rt = reflections[currentFrame]; + if (rt.width > 0) { + ImGui::Text("Reflection RT: %ux%u", rt.width, rt.height); + } + } + } + + // === RAY QUERY-SPECIFIC OPTIONS === + if (currentRenderMode == RenderMode::RayQuery && rayQueryEnabled && accelerationStructureEnabled) { + ImGui::Separator(); + ImGui::Text("Ray Query Status:"); + + // Show acceleration structure status + if (!!*tlasStructure.handle) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Acceleration Structures: Built (%zu meshes)", blasStructures.size()); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Acceleration Structures: Not built"); + } + + ImGui::Spacing(); + ImGui::Text("Ray Query Features:"); + ImGui::Checkbox("Enable Hard Shadows", &enableRayQueryShadows); + if (enableRayQueryShadows) { + ImGui::SliderInt("Shadow samples", &rayQueryShadowSampleCount, 1, 32); + ImGui::SliderFloat("Shadow softness (fraction of range)", &rayQueryShadowSoftness, 0.0f, 0.2f, "%.3f"); + } + ImGui::Checkbox("Enable Reflections", &enableRayQueryReflections); + ImGui::Checkbox("Enable Transparency/Refraction", &enableRayQueryTransparency); + ImGui::SliderInt("Max secondary bounces", &rayQueryMaxBounces, 0, 10); + // Thick-glass realism controls + ImGui::Separator(); + ImGui::Text("Thick Glass"); + ImGui::Checkbox("Enable Thick Glass", &enableThickGlass); + ImGui::SliderFloat("Thickness Clamp (m)", &thickGlassThicknessClamp, 0.0f, 0.5f, "%.3f"); + ImGui::SliderFloat("Absorption Scale", &thickGlassAbsorptionScale, 0.0f, 4.0f, "%.2f"); + } + + // === SHARED OPTIONS (BOTH MODES) === + ImGui::Separator(); + ImGui::Text("Culling & LOD:"); + if (ImGui::Checkbox("Frustum culling", &enableFrustumCulling)) { + // no-op, takes effect immediately + } + if (ImGui::Checkbox("Distance LOD (projected-size skip)", &enableDistanceLOD)) { + } + ImGui::SliderFloat("LOD threshold opaque (px)", &lodPixelThresholdOpaque, 0.5f, 8.0f, "%.1f"); + ImGui::SliderFloat("LOD threshold transparent (px)", &lodPixelThresholdTransparent, 0.5f, 12.0f, "%.1f"); + // Anisotropy control (recreate samplers on change) + { + float deviceMaxAniso = physicalDevice.getProperties().limits.maxSamplerAnisotropy; + if (ImGui::SliderFloat("Sampler max anisotropy", &samplerMaxAnisotropy, 1.0f, deviceMaxAniso, "%.1f")) { + // Recreate samplers for all textures to apply new anisotropy + std::unique_lock texLock(textureResourcesMutex); + for (auto& kv : textureResources) { + createTextureSampler(kv.second); + } + // Default texture + createTextureSampler(defaultTextureResources); + } + } + if (lastCullingVisibleCount + lastCullingCulledCount > 0) { + ImGui::Text("Culling: visible=%u, culled=%u", lastCullingVisibleCount, lastCullingCulledCount); + } + + // Basic tone mapping controls + ImGui::Separator(); + ImGui::Text("Tone Mapping & Tuning:"); + ImGui::SliderFloat("Reflection intensity", &reflectionIntensity, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Exposure", &exposure, 0.1f, 4.0f, "%.2f"); + ImGui::SliderFloat("Gamma", &gamma, 1.6f, 2.6f, "%.2f"); + } + ImGui::End(); + } + + // Rasterization rendering: only execute if ray query did not render this frame. + if (!rayQueryRenderedThisFrame) { + // Optional: render planar reflections first + /* + if (enablePlanarReflections) { + glm::vec4 planeWS(0.0f, 1.0f, 0.0f, 0.0f); + renderReflectionPass(commandBuffers[currentFrame], planeWS, camera, opaqueJobs); + } + */ + + // Sort transparent entities back-to-front for correct blending of nested glass/liquids + if (!transparentJobs.empty()) { + glm::vec3 camPos = camera ? camera->GetPosition() : glm::vec3(0.0f); + std::ranges::sort(transparentJobs, + [camPos](const RenderJob& a, const RenderJob& b) { + glm::vec3 pa = a.transformComp ? a.transformComp->GetPosition() : glm::vec3(0.0f); + glm::vec3 pb = b.transformComp ? b.transformComp->GetPosition() : glm::vec3(0.0f); + float da2 = glm::length2(pa - camPos); + float db2 = glm::length2(pb - camPos); + if (da2 != db2) return da2 > db2; + if (a.entityRes->cachedIsLiquid != b.entityRes->cachedIsLiquid) return a.entityRes->cachedIsLiquid; + return a.entity < b.entity; + }); + } + + + // Track whether we executed a depth pre-pass this frame (used to choose depth load op and pipeline state) + bool didOpaqueDepthPrepass = false; + + // Optional Forward+ depth pre-pass for opaque geometry + if (useForwardPlus) { + if (!opaqueJobs.empty()) { + // Transition depth image for attachment write (Sync2) + vk::ImageMemoryBarrier2 depthBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = {}, + .dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests, + .dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eDepthAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *depthImage, + .subresourceRange = {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1} + }; + vk::DependencyInfo depInfoDepth{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &depthBarrier2}; + commandBuffers[currentFrame].pipelineBarrier2(depInfoDepth); + + // Depth-only rendering + vk::RenderingAttachmentInfo depthOnlyAttachment{.imageView = *depthImageView, .imageLayout = vk::ImageLayout::eDepthAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearDepthStencilValue{1.0f, 0}}; + vk::RenderingInfo depthOnlyInfo{.renderArea = vk::Rect2D({0, 0}, swapChainExtent), .layerCount = 1, .colorAttachmentCount = 0, .pColorAttachments = nullptr, .pDepthAttachment = &depthOnlyAttachment}; + commandBuffers[currentFrame].beginRendering(depthOnlyInfo); + vk::Viewport viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + vk::Rect2D scissor({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); + + // Bind depth pre-pass pipeline + if (!!*depthPrepassPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *depthPrepassPipeline); + } + + for (const auto& job : opaqueJobs) { + if (job.isAlphaMasked) continue; + + // Bind geometry + std::array buffers = {*job.meshRes->vertexBuffer, *job.entityRes->instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*job.meshRes->indexBuffer, 0, vk::IndexType::eUint32); + + // Bind descriptor set (PBR set 0) + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *pbrPipelineLayout, + 0, + *job.entityRes->pbrDescriptorSets[currentFrame], + nullptr); + + // Issue draw + uint32_t instanceCount = std::max(1u, static_cast(job.meshComp->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(job.meshRes->indexCount, instanceCount, 0, 0, 0); + } + + commandBuffers[currentFrame].endRendering(); + + // Barrier to ensure depth is visible for subsequent passes (Sync2) + vk::ImageMemoryBarrier2 depthToRead2{ + .srcStageMask = vk::PipelineStageFlagBits2::eLateFragmentTests, + .srcAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests, + .dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead, + .oldLayout = vk::ImageLayout::eDepthAttachmentOptimal, + .newLayout = vk::ImageLayout::eDepthAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *depthImage, + .subresourceRange = {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1} + }; + vk::DependencyInfo depInfoDepthToRead{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &depthToRead2}; + commandBuffers[currentFrame].pipelineBarrier2(depInfoDepthToRead); + + didOpaqueDepthPrepass = true; + } + + // Forward+ compute culling based on current camera and screen tiles + uint32_t tilesX = (swapChainExtent.width + forwardPlusTileSizeX - 1) / forwardPlusTileSizeX; + uint32_t tilesY = (swapChainExtent.height + forwardPlusTileSizeY - 1) / forwardPlusTileSizeY; + + // Lights already extracted at frame start - use lastFrameLightCount for Forward+ params + glm::mat4 view = camera->GetViewMatrix(); + glm::mat4 proj = camera->GetProjectionMatrix(); + proj[1][1] *= -1.0f; + float nearZ = camera->GetNearPlane(); + float farZ = camera->GetFarPlane(); + updateForwardPlusParams(currentFrame, view, proj, lastFrameLightCount, tilesX, tilesY, forwardPlusSlicesZ, nearZ, farZ); + // As a last guard before dispatch, make sure compute binding 0 is valid for this frame + refreshForwardPlusComputeLightsBindingForFrame(currentFrame); + + dispatchForwardPlus(commandBuffers[currentFrame], tilesX, tilesY, forwardPlusSlicesZ); + } + + // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE + // Transition off-screen color to attachment write (Sync2). On first use after creation or after switching + // from a mode that never produced this image, the layout may still be UNDEFINED. + vk::ImageLayout oscOldLayout = vk::ImageLayout::eUndefined; + vk::PipelineStageFlags2 oscSrcStage = vk::PipelineStageFlagBits2::eTopOfPipe; + vk::AccessFlags2 oscSrcAccess = vk::AccessFlagBits2::eNone; + if (currentFrame < opaqueSceneColorImageLayouts.size()) { + oscOldLayout = opaqueSceneColorImageLayouts[currentFrame]; + if (oscOldLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + oscSrcStage = vk::PipelineStageFlagBits2::eFragmentShader; + oscSrcAccess = vk::AccessFlagBits2::eShaderRead; + } else if (oscOldLayout == vk::ImageLayout::eColorAttachmentOptimal) { + oscSrcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; + oscSrcAccess = vk::AccessFlagBits2::eColorAttachmentWrite; + } else { + oscOldLayout = vk::ImageLayout::eUndefined; + oscSrcStage = vk::PipelineStageFlagBits2::eTopOfPipe; + oscSrcAccess = vk::AccessFlagBits2::eNone; + } + } + vk::ImageMemoryBarrier2 oscToColor2{ + .srcStageMask = oscSrcStage, + .srcAccessMask = oscSrcAccess, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead, + .oldLayout = oscOldLayout, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *opaqueSceneColorImages[currentFrame], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depOscToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &oscToColor2}; + commandBuffers[currentFrame].pipelineBarrier2(depOscToColor); + if (currentFrame < opaqueSceneColorImageLayouts.size()) { + opaqueSceneColorImageLayouts[currentFrame] = vk::ImageLayout::eColorAttachmentOptimal; + } + // PASS 1: OFF-SCREEN COLOR (Opaque) + // Clear the off-screen target at the start of opaque rendering to a neutral black background + vk::RenderingAttachmentInfo colorAttachment{.imageView = *opaqueSceneColorImageViews[currentFrame], .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array < float, 4 >{0.0f, 0.0f, 0.0f, 1.0f})}; + depthAttachment.imageView = *depthImageView; + depthAttachment.loadOp = (didOpaqueDepthPrepass) ? vk::AttachmentLoadOp::eLoad : vk::AttachmentLoadOp::eClear; + vk::RenderingInfo passInfo{.renderArea = vk::Rect2D({0, 0}, swapChainExtent), .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &colorAttachment, .pDepthAttachment = &depthAttachment}; + commandBuffers[currentFrame].beginRendering(passInfo); + vk::Viewport viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + vk::Rect2D scissor({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); { + uint32_t opaqueDrawsThisPass = 0; + for (const auto& job : opaqueJobs) { + bool useBasic = (imguiSystem && !imguiSystem->IsPBREnabled()); + vk::raii::Pipeline* selectedPipeline = nullptr; + vk::raii::PipelineLayout* selectedLayout = nullptr; + if (useBasic) { + selectedPipeline = &graphicsPipeline; + selectedLayout = &pipelineLayout; + } else { + // If masked, we need depth writes with alpha test; otherwise, after-prepass read-only is fine. + if (job.isAlphaMasked) { + selectedPipeline = &pbrGraphicsPipeline; // writes depth, compare Less + } else { + selectedPipeline = didOpaqueDepthPrepass && !!*pbrPrepassGraphicsPipeline ? &pbrPrepassGraphicsPipeline : &pbrGraphicsPipeline; + } + selectedLayout = &pbrPipelineLayout; + } + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + + std::array buffers = {*job.meshRes->vertexBuffer, *job.entityRes->instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*job.meshRes->indexBuffer, 0, vk::IndexType::eUint32); + + auto* descSetsPtr = useBasic ? &job.entityRes->basicDescriptorSets : &job.entityRes->pbrDescriptorSets; + if (descSetsPtr->empty() || currentFrame >= descSetsPtr->size()) { + continue; + } + + if (useBasic) { + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **selectedLayout, + 0, + {*(*descSetsPtr)[currentFrame]}, + {}); + } else { + vk::DescriptorSet set1Opaque = (transparentDescriptorSets.empty() || IsLoading()) + ? *transparentFallbackDescriptorSets[currentFrame] + : *transparentDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **selectedLayout, + 0, + {*(*descSetsPtr)[currentFrame], set1Opaque}, + {}); + + commandBuffers[currentFrame].pushConstants(**selectedLayout, vk::ShaderStageFlagBits::eFragment, 0, {job.entityRes->cachedMaterialProps}); + } + uint32_t instanceCount = std::max(1u, static_cast(job.meshComp->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(job.meshRes->indexCount, instanceCount, 0, 0, 0); + ++opaqueDrawsThisPass; + } + } + commandBuffers[currentFrame].endRendering(); + // PASS 1b: PRESENT – composite path + { + // Transition off-screen to SHADER_READ for sampling (Sync2) + vk::ImageMemoryBarrier2 opaqueToSample2{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *opaqueSceneColorImages[currentFrame], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depOpaqueToSample{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &opaqueToSample2}; + commandBuffers[currentFrame].pipelineBarrier2(depOpaqueToSample); + if (currentFrame < opaqueSceneColorImageLayouts.size()) { + opaqueSceneColorImageLayouts[currentFrame] = vk::ImageLayout::eShaderReadOnlyOptimal; + } + + // Make the swapchain image ready for color attachment output and clear it (Sync2) + vk::ImageMemoryBarrier2 swapchainToColor2{ + .srcStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depSwapchainToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &swapchainToColor2}; + commandBuffers[currentFrame].pipelineBarrier2(depSwapchainToColor); + + // Begin rendering to swapchain for composite + colorAttachments[0].imageView = *swapChainImageViews[imageIndex]; + colorAttachments[0].loadOp = vk::AttachmentLoadOp::eClear; // clear before composing base layer (full-screen composite overwrites all pixels) + depthAttachment.loadOp = vk::AttachmentLoadOp::eDontCare; // no depth for composite + renderingInfo.renderArea = vk::Rect2D({0, 0}, swapChainExtent); + // IMPORTANT: Composite pass does not use a depth attachment. Avoid binding it to satisfy dynamic rendering VUIDs. + auto savedDepthPtr = renderingInfo.pDepthAttachment; // save to restore later + renderingInfo.pDepthAttachment = nullptr; + commandBuffers[currentFrame].beginRendering(renderingInfo); + + // Bind composite pipeline + if (!!*compositePipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *compositePipeline); + } + vk::Viewport vp(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, vp); + vk::Rect2D sc({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, sc); + + // Bind descriptor set 0 for the composite. During loading, force fallback to avoid sampling uninitialized off-screen color. + vk::DescriptorSet setComposite = (transparentDescriptorSets.empty() || IsLoading()) + ? *transparentFallbackDescriptorSets[currentFrame] + : *transparentDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + *compositePipelineLayout, + 0, + {setComposite}, + {}); + + // Push exposure/gamma and sRGB flag + struct CompositePush { + float exposure; + float gamma; + int outputIsSRGB; + float _pad; + } pc{}; + pc.exposure = std::clamp(this->exposure, 0.2f, 4.0f); + pc.gamma = this->gamma; + pc.outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; + commandBuffers[currentFrame].pushConstants(*compositePipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, pc); + + // Draw fullscreen triangle + commandBuffers[currentFrame].draw(3, 1, 0, 0); + + commandBuffers[currentFrame].endRendering(); + // Restore depth attachment pointer for subsequent passes + renderingInfo.pDepthAttachment = savedDepthPtr; + } + // PASS 2: RENDER TRANSPARENT OBJECTS TO THE SWAPCHAIN + { + // Ensure depth attachment is bound again for the transparent pass + renderingInfo.pDepthAttachment = &depthAttachment; + colorAttachments[0].imageView = *swapChainImageViews[imageIndex]; + colorAttachments[0].loadOp = vk::AttachmentLoadOp::eLoad; + depthAttachment.loadOp = vk::AttachmentLoadOp::eLoad; + renderingInfo.renderArea = vk::Rect2D({0, 0}, swapChainExtent); + commandBuffers[currentFrame].beginRendering(renderingInfo); + commandBuffers[currentFrame].setViewport(0, viewport); + commandBuffers[currentFrame].setScissor(0, scissor); + + if (!transparentJobs.empty()) { + currentLayout = &pbrTransparentPipelineLayout; + vk::raii::Pipeline* activeTransparentPipeline = nullptr; + + for (const auto& job : transparentJobs) { + vk::raii::Pipeline* desiredPipeline = job.entityRes->cachedIsGlass ? &glassGraphicsPipeline : &pbrBlendGraphicsPipeline; + if (desiredPipeline != activeTransparentPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **desiredPipeline); + activeTransparentPipeline = desiredPipeline; + } + + std::array buffers = {*job.meshRes->vertexBuffer, *job.entityRes->instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*job.meshRes->indexBuffer, 0, vk::IndexType::eUint32); + + vk::DescriptorSet set1 = (transparentDescriptorSets.empty() || IsLoading()) + ? *transparentFallbackDescriptorSets[currentFrame] + : *transparentDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **currentLayout, + 0, + {*job.entityRes->pbrDescriptorSets[currentFrame], set1}, + {}); + + MaterialProperties pushConstants = job.entityRes->cachedMaterialProps; + if (job.entityRes->cachedIsLiquid) { + pushConstants.transmissionFactor = 0.0f; + } + commandBuffers[currentFrame].pushConstants < MaterialProperties > (**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, { + pushConstants + } + ) + ; + uint32_t instanceCountT = std::max(1u, static_cast(job.meshComp->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(job.meshRes->indexCount, instanceCountT, 0, 0, 0); + } + } + // End transparent rendering pass before any layout transitions (even if no transparent draws) + commandBuffers[currentFrame].endRendering(); + } { + // Screenshot and final present transition are handled in rasterization path only + // Ray query path handles these separately + + // Final layout transition for present (rasterization path only) + { + vk::ImageMemoryBarrier2 presentBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eNone, + .dstAccessMask = {}, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depToPresentFinal{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &presentBarrier2}; + commandBuffers[currentFrame].pipelineBarrier2(depToPresentFinal); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = presentBarrier2.newLayout; + } + } + } // skip rasterization when ray query has rendered + + // Render ImGui UI overlay AFTER rasterization/ray query (must always execute regardless of render mode) + // ImGui expects Render() to be called every frame after NewFrame() - skipping it causes hangs + if (imguiSystem && !imguiSystem->IsFrameRendered()) { + // When ray query renders, swapchain is in PRESENT layout with valid content. + // When rasterization renders, swapchain is also in PRESENT layout with valid content. + // Transition to COLOR_ATTACHMENT with loadOp=eLoad to preserve existing pixels for ImGui overlay. + vk::ImageMemoryBarrier2 presentToColor{ + .srcStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead, + .oldLayout = (imageIndex < swapChainImageLayouts.size()) ? swapChainImageLayouts[imageIndex] : vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depInfo{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &presentToColor}; + commandBuffers[currentFrame].pipelineBarrier2(depInfo); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = presentToColor.newLayout; + + // Begin a dedicated render pass for ImGui (UI overlay) + vk::RenderingAttachmentInfo imguiColorAttachment{ + .imageView = *swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eLoad, // Load existing content + .storeOp = vk::AttachmentStoreOp::eStore + }; + vk::RenderingInfo imguiRenderingInfo{ + .renderArea = vk::Rect2D({0, 0}, swapChainExtent), + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &imguiColorAttachment, + .pDepthAttachment = nullptr + }; + commandBuffers[currentFrame].beginRendering(imguiRenderingInfo); + + imguiSystem->Render(commandBuffers[currentFrame], currentFrame); + + commandBuffers[currentFrame].endRendering(); + + // Transition swapchain back to PRESENT layout after ImGui renders + vk::ImageMemoryBarrier2 colorToPresent{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eBottomOfPipe, + .dstAccessMask = vk::AccessFlagBits2::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} + }; + vk::DependencyInfo depInfoBack{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &colorToPresent}; + commandBuffers[currentFrame].pipelineBarrier2(depInfoBack); + if (imageIndex < swapChainImageLayouts.size()) + swapChainImageLayouts[imageIndex] = colorToPresent.newLayout; + } + + commandBuffers[currentFrame].end(); + isRecordingCmd.store(false, std::memory_order_relaxed); + + // Submit and present (Synchronization 2) + uint64_t uploadsValueToWait = uploadTimelineLastSubmitted.load(std::memory_order_relaxed); + + // Use acquireSemaphoreIndex for imageAvailable semaphore (same as we used in acquireNextImage) + // Use imageIndex for renderFinished semaphore (matches the image being presented) + + std::array waitInfos = { + vk::SemaphoreSubmitInfo{ + .semaphore = *imageAvailableSemaphores[acquireSemaphoreIndex], + .value = 0, + .stageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .deviceIndex = 0 + }, + vk::SemaphoreSubmitInfo{ + .semaphore = *uploadsTimeline, + .value = uploadsValueToWait, + .stageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .deviceIndex = 0 + } + }; + + vk::CommandBufferSubmitInfo cmdInfo{.commandBuffer = *commandBuffers[currentFrame], .deviceMask = 0}; + vk::SemaphoreSubmitInfo signalInfo{.semaphore = *renderFinishedSemaphores[imageIndex], .value = 0, .stageMask = vk::PipelineStageFlagBits2::eAllGraphics, .deviceIndex = 0}; + vk::SubmitInfo2 submit2{ + .waitSemaphoreInfoCount = static_cast(waitInfos.size()), + .pWaitSemaphoreInfos = waitInfos.data(), + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &cmdInfo, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &signalInfo + }; + + if (framebufferResized.load(std::memory_order_relaxed)) { + vk::SubmitInfo2 emptySubmit2{}; { + std::lock_guard lock(queueMutex); + graphicsQueue.submit2(emptySubmit2, *inFlightFences[currentFrame]); + } + recreateSwapChain(); + return; + } + + // Update watchdog BEFORE queue submit because submit can block waiting for GPU + // This proves frame CPU work is complete even if GPU queue is busy + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); { + std::lock_guard lock(queueMutex); + graphicsQueue.submit2(submit2, *inFlightFences[currentFrame]); + } + + vk::PresentInfoKHR presentInfo{.waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex}; + vk::Result presentResult = vk::Result::eSuccess; + try { + std::lock_guard lock(queueMutex); + presentResult = presentQueue.presentKHR(presentInfo); + } catch (const vk::OutOfDateKHRError&) { + framebufferResized.store(true, std::memory_order_relaxed); + } + if (presentResult == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { + framebufferResized.store(false, std::memory_order_relaxed); + recreateSwapChain(); + } else if (presentResult != vk::Result::eSuccess) { + throw std::runtime_error("Failed to present swap chain image"); + } + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} + +// Public toggle APIs for planar reflections (keyboard/UI) +void Renderer::SetPlanarReflectionsEnabled(bool enabled) { + // Flip mode and mark resources dirty so RTs are created/destroyed at the next safe point + enablePlanarReflections = enabled; + reflectionResourcesDirty = true; +} + +void Renderer::TogglePlanarReflections() { + SetPlanarReflectionsEnabled(!enablePlanarReflections); +} diff --git a/attachments/openxr_engine/renderer_resources.cpp b/attachments/openxr_engine/renderer_resources.cpp new file mode 100644 index 000000000..17a28d277 --- /dev/null +++ b/attachments/openxr_engine/renderer_resources.cpp @@ -0,0 +1,4074 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "mesh_component.h" +#include "model_loader.h" +#include "renderer.h" +#include "transform_component.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. + +// KTX2 support +#include +#include + +// This file contains resource-related methods from the Renderer class + +// Define shared default PBR texture identifiers (static constants) +const std::string Renderer::SHARED_DEFAULT_ALBEDO_ID = "__shared_default_albedo__"; +const std::string Renderer::SHARED_DEFAULT_NORMAL_ID = "__shared_default_normal__"; +const std::string Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID = "__shared_default_metallic_roughness__"; +const std::string Renderer::SHARED_DEFAULT_OCCLUSION_ID = "__shared_default_occlusion__"; +const std::string Renderer::SHARED_DEFAULT_EMISSIVE_ID = "__shared_default_emissive__"; +const std::string Renderer::SHARED_BRIGHT_RED_ID = "__shared_bright_red__"; + +// Create depth resources +bool Renderer::createDepthResources() { + try { + // Find depth format + vk::Format depthFormat = findDepthFormat(); + uint32_t layers = xrMode ? 2 : 1; + + // Use non-pooled createImage for depth as memoryPool doesn't support layers yet + auto [image, memory] = createImage( + swapChainExtent.width, + swapChainExtent.height, + depthFormat, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment, + vk::MemoryPropertyFlagBits::eDeviceLocal, + layers); + + depthImage = std::move(image); + depthImageMemory = std::move(memory); + + // Create depth image view (with layerCount=layers) + depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth, 1, layers); + + // Transition depth image layout + transitionImageLayout( + *depthImage, + depthFormat, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthStencilAttachmentOptimal, + 1, + layers); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create depth resources: " << e.what() << std::endl; + return false; + } +} + +// Helper: coerce an sRGB/UNORM variant of a given VkFormat while preserving block type where possible +static vk::Format CoerceFormatSRGB(vk::Format fmt, bool wantSRGB) { + switch (fmt) { + case vk::Format::eR8G8B8A8Unorm: + return wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + case vk::Format::eR8G8B8A8Srgb: + return wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + + case vk::Format::eBc1RgbUnormBlock: + return wantSRGB ? vk::Format::eBc1RgbSrgbBlock : vk::Format::eBc1RgbUnormBlock; + case vk::Format::eBc1RgbSrgbBlock: + return wantSRGB ? vk::Format::eBc1RgbSrgbBlock : vk::Format::eBc1RgbUnormBlock; + case vk::Format::eBc1RgbaUnormBlock: + return wantSRGB ? vk::Format::eBc1RgbaSrgbBlock : vk::Format::eBc1RgbaUnormBlock; + case vk::Format::eBc1RgbaSrgbBlock: + return wantSRGB ? vk::Format::eBc1RgbaSrgbBlock : vk::Format::eBc1RgbaUnormBlock; + + case vk::Format::eBc2UnormBlock: + return wantSRGB ? vk::Format::eBc2SrgbBlock : vk::Format::eBc2UnormBlock; + case vk::Format::eBc2SrgbBlock: + return wantSRGB ? vk::Format::eBc2SrgbBlock : vk::Format::eBc2UnormBlock; + + case vk::Format::eBc3UnormBlock: + return wantSRGB ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + case vk::Format::eBc3SrgbBlock: + return wantSRGB ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + + case vk::Format::eBc7UnormBlock: + return wantSRGB ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + case vk::Format::eBc7SrgbBlock: + return wantSRGB ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + + default: + return fmt; + } +} + +// Create texture image +bool Renderer::createTextureImage(const std::string& texturePath_, TextureResources& resources) { + try { + ensureThreadLocalVulkanInit(); + const std::string textureId = ResolveTextureId(texturePath_); + // Check if texture already exists + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(textureId); + if (it != textureResources.end()) { + // Texture already loaded and cached; leave cache intact and return success + return true; + } + } + + // Resolve on-disk path (may differ from logical ID) + std::string resolvedPath = textureId; + + // Ensure command pool is initialized before any GPU work + if (!*commandPool) { + std::cerr << "createTextureImage: commandPool not initialized yet for '" << textureId << "'" << std::endl; + return false; + } + + // Per-texture de-duplication (serialize loads of the same texture ID only) + { + std::unique_lock lk(textureLoadStateMutex); + while (texturesLoading.contains(textureId)) { + textureLoadStateCv.wait(lk); + } + } + // Double-check cache after the wait + { + std::shared_lock texLock(textureResourcesMutex); + auto it2 = textureResources.find(textureId); + if (it2 != textureResources.end()) { + return true; + } + } + // Mark as loading and ensure we notify on all exit paths + { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.insert(textureId); + } + auto _loadingGuard = std::unique_ptr>(reinterpret_cast(1), + [this, textureId](void*) { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.erase(textureId); + textureLoadStateCv.notify_all(); + }); + + // Check if this is a KTX2 file + bool isKtx2 = resolvedPath.ends_with(".ktx2"); + + // If it's a KTX2 texture but the path doesn't exist, try common fallback filename variants + if (isKtx2) { + std::filesystem::path origPath(resolvedPath); + if (!std::filesystem::exists(origPath)) { + std::string fname = origPath.filename().string(); + std::string dir = origPath.parent_path().string(); + auto tryCandidate = [&](const std::string& candidateName) -> bool { + std::filesystem::path cand = std::filesystem::path(dir) / candidateName; + if (std::filesystem::exists(cand)) { + std::cout << "Resolved missing texture '" << resolvedPath << "' to existing file '" << cand.string() << "'" << std::endl; + resolvedPath = cand.string(); + return true; + } + return false; + }; + // Known suffix variants near the end of filename before extension + // Examples: *_c.ktx2, *_d.ktx2, *_cm.ktx2, *_diffuse.ktx2, *_basecolor.ktx2, *_albedo.ktx2 + std::vector suffixes = {"_c", "_d", "_cm", "_diffuse", "_basecolor", "_albedo"}; + // If filename matches one known suffix, try others + for (const auto& s : suffixes) { + std::string key = s + ".ktx2"; + if (fname.ends_with(key)) { + std::string prefix = fname.substr(0, fname.size() - key.size()); + for (const auto& alt : suffixes) { + if (alt == s) + continue; + std::string candName = prefix + alt + ".ktx2"; + if (tryCandidate(candName)) { + isKtx2 = true; + break; + } + } + break; // Only replace last suffix occurrence + } + } + } + } + + int texWidth, texHeight, texChannels; + unsigned char* pixels = nullptr; + ktxTexture2* ktxTex = nullptr; + vk::DeviceSize imageSize; + + // Track KTX2 transcoding state across the function scope (BasisU only) + bool wasTranscoded = false; + // Track KTX2 header-provided VkFormat (0 == VK_FORMAT_UNDEFINED) + uint32_t headerVkFormatRaw = 0; + + uint32_t mipLevels = 1; + std::vector copyRegions; + + if (isKtx2) { + // Load KTX2 file + KTX_error_code result = ktxTexture2_CreateFromNamedFile(resolvedPath.c_str(), + KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &ktxTex); + if (result != KTX_SUCCESS) { + // Retry with sibling suffix variants if file exists but cannot be parsed/opened + std::filesystem::path origPath(resolvedPath); + std::string fname = origPath.filename().string(); + std::string dir = origPath.parent_path().string(); + auto tryLoad = [&](const std::string& candidateName) -> bool { + std::filesystem::path cand = std::filesystem::path(dir) / candidateName; + if (std::filesystem::exists(cand)) { + std::string candStr = cand.string(); + std::cout << "Retrying KTX2 load with sibling candidate '" << candStr << "' for original '" << resolvedPath << "'" << std::endl; + result = ktxTexture2_CreateFromNamedFile(candStr.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result == KTX_SUCCESS) { + resolvedPath = candStr; // Use the successfully opened candidate + return true; + } + } + return false; + }; + // Known suffix variants near the end of filename before extension + std::vector suffixes = {"_c", "_d", "_cm", "_diffuse", "_basecolor", "_albedo"}; + for (const auto& s : suffixes) { + std::string key = s + ".ktx2"; + if (fname.ends_with(key)) { + std::string prefix = fname.substr(0, fname.size() - key.size()); + bool loaded = false; + for (const auto& alt : suffixes) { + if (alt == s) + continue; + std::string candName = prefix + alt + ".ktx2"; + if (tryLoad(candName)) { + loaded = true; + break; + } + } + if (loaded) + break; + } + } + } + + // Bail out if we still failed to load + if (result != KTX_SUCCESS || ktxTex == nullptr) { + std::cerr << "Failed to load KTX2 texture: " << resolvedPath << " (error: " << result << ")" << std::endl; + return false; + } + + // Read header-provided vkFormat (if already GPU-compressed/transcoded offline) + headerVkFormatRaw = static_cast(ktxTex->vkFormat); + + // Check if the texture needs BasisU transcoding; prefer GPU-compressed targets to save VRAM + wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); + if (wasTranscoded) { + // Select a compressed target supported by the device (prefer BC7 RGBA, then BC3 RGBA, then BC1 RGB) + auto supportsFormat = [&](vk::Format f) { + auto props = physicalDevice.getFormatProperties(f); + return static_cast(props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage); + }; + bool wantSrgb = (Renderer::determineTextureFormat(resolvedPath) == vk::Format::eR8G8B8A8Srgb); + KTX_error_code tcErr = KTX_SUCCESS; + if (supportsFormat(vk::Format::eBc7UnormBlock) || supportsFormat(vk::Format::eBc7SrgbBlock)) { + tcErr = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_BC7_RGBA, 0); + } else if (supportsFormat(vk::Format::eBc3UnormBlock) || supportsFormat(vk::Format::eBc3SrgbBlock)) { + tcErr = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_BC3_RGBA, 0); + } else if (supportsFormat(vk::Format::eBc1RgbUnormBlock) || supportsFormat(vk::Format::eBc1RgbSrgbBlock)) { + tcErr = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_BC1_RGB, 0); + } else { + // Fallback to RGBA32 if no BC formats are supported + tcErr = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); + } + if (tcErr != KTX_SUCCESS) { + std::cerr << "Failed to transcode KTX2 BasisU texture: " << resolvedPath << " (error: " << tcErr << ")" << std::endl; + ktxTexture_Destroy(reinterpret_cast(ktxTex)); + return false; + } + } + + texWidth = ktxTex->baseWidth; + texHeight = ktxTex->baseHeight; + texChannels = 4; // logical channels; compressed size handled by libktx + + // Use all levels present in the KTX container + mipLevels = std::max(1u, ktxTex->numLevels); + + // Total data size across all mip levels + imageSize = ktxTexture_GetDataSize(reinterpret_cast(ktxTex)); + + // Build copy regions for every mip level in the file + copyRegions.clear(); + copyRegions.reserve(mipLevels); + for (uint32_t level = 0; level < mipLevels; ++level) { + ktx_size_t levelOffset = 0; + ktxTexture_GetImageOffset(reinterpret_cast(ktxTex), level, 0, 0, &levelOffset); + uint32_t w = std::max(1u, static_cast(texWidth) >> level); + uint32_t h = std::max(1u, static_cast(texHeight) >> level); + copyRegions.push_back({ + .bufferOffset = static_cast(levelOffset), + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = level, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {w, h, 1} + }); + } + } else { + // Non-KTX texture loading via file path is disabled to simplify pipeline. + std::cerr << "Unsupported non-KTX2 texture path: " << textureId << std::endl; + return false; + } + + // Create staging buffer + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Copy pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + + if (isKtx2) { + // Copy entire KTX2 image data blob (all mip levels) + const uint8_t* allData = ktxTexture_GetData(reinterpret_cast(ktxTex)); + const ktx_size_t dataSz = ktxTexture_GetDataSize(reinterpret_cast(ktxTex)); + memcpy(data, allData, static_cast(dataSz)); + } else { + // Copy regular image data + memcpy(data, pixels, static_cast(imageSize)); + } + + stagingBufferMemory.unmapMemory(); + + // Determine appropriate texture format + vk::Format textureFormat; + const bool wantSRGB = (Renderer::determineTextureFormat(textureId) == vk::Format::eR8G8B8A8Srgb); + bool alphaMaskedHint = false; + if (isKtx2) { + // If the KTX2 provided a valid VkFormat and we did NOT transcode, respect its block type + // but coerce the sRGB/UNORM variant based on texture usage (baseColor vs data maps) + if (!wasTranscoded) { + VkFormat headerFmt = static_cast(headerVkFormatRaw); + if (headerFmt != VK_FORMAT_UNDEFINED) { + textureFormat = CoerceFormatSRGB(static_cast(headerFmt), wantSRGB); + } else { + textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + } + // Can't easily scan alpha in compressed formats here; leave hint at default false + } else { + // We transcoded; choose a Vulkan format matching the transcode target (we requested BC7/BC3/BC1 or RGBA32 fallback) + // There is no direct query from KTX for chosen VkFormat after transcoding, so infer by capabilities using our preference order. + bool wantSRGB2 = wantSRGB; + if (!!physicalDevice.getFormatProperties(vk::Format::eBc7UnormBlock).optimalTilingFeatures || !!physicalDevice.getFormatProperties(vk::Format::eBc7SrgbBlock).optimalTilingFeatures) { + textureFormat = wantSRGB2 ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + } else if (!!physicalDevice.getFormatProperties(vk::Format::eBc3UnormBlock).optimalTilingFeatures || !!physicalDevice.getFormatProperties(vk::Format::eBc3SrgbBlock).optimalTilingFeatures) { + textureFormat = wantSRGB2 ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + } else if (!!physicalDevice.getFormatProperties(vk::Format::eBc1RgbUnormBlock).optimalTilingFeatures || !!physicalDevice.getFormatProperties(vk::Format::eBc1RgbSrgbBlock).optimalTilingFeatures) { + textureFormat = wantSRGB2 ? vk::Format::eBc1RgbSrgbBlock : vk::Format::eBc1RgbUnormBlock; + } else { + // Fallback to uncompressed RGBA + textureFormat = wantSRGB2 ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + // We have CPU-visible RGBA data; detect alpha for masked hint + ktx_size_t offsetScan = 0; + ktxTexture_GetImageOffset(reinterpret_cast(ktxTex), 0, 0, 0, &offsetScan); + const uint8_t* rgba = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offsetScan; + size_t pixelCount = static_cast(texWidth) * static_cast(texHeight); + for (size_t i = 0; i < pixelCount; ++i) { + if (rgba[i * 4 + 3] < 250) { + alphaMaskedHint = true; + break; + } + } + } + } + } else { + textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; + } + + // Now that we're done reading libktx data, destroy the KTX texture to avoid leaks + if (isKtx2 && ktxTex) { + ktxTexture_Destroy(reinterpret_cast(ktxTex)); + ktxTex = nullptr; + } + + // Create texture image using memory pool + bool differentFamilies = queueFamilyIndices.graphicsFamily.value() != queueFamilyIndices.transferFamily.value(); + std::vector families; + if (differentFamilies) { + families = {queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.transferFamily.value()}; + } + // KTX2 mip levels are set above (line 306); mipLevels already reflects what the file contains + // KTX2 files come with pre-generated mips, so we don't need TRANSFER_SRC for blit generation + vk::ImageUsageFlags usageFlags = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + + // Create image with OOM fallback: retry with mipLevels=1 and reduced usage if needed + try { + auto [textureImg, textureImgAllocation] = createImagePooled( + texWidth, + texHeight, + textureFormat, + vk::ImageTiling::eOptimal, + usageFlags, + vk::MemoryPropertyFlagBits::eDeviceLocal, + /*mipLevels*/ + mipLevels, + differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, + families); + resources.textureImage = std::move(textureImg); + resources.textureImageAllocation = std::move(textureImgAllocation); + } catch (const std::exception& e) { + std::cerr << "Image allocation failed (" << resolvedPath << "): " << e.what() << ". Retrying with mipLevels=1..." << std::endl; + // Retry with a single mip level and no TRANSFER_SRC usage to reduce memory pressure + mipLevels = 1; + usageFlags &= ~vk::ImageUsageFlagBits::eTransferSrc; + auto [textureImg2, textureImgAllocation2] = createImagePooled( + texWidth, + texHeight, + textureFormat, + vk::ImageTiling::eOptimal, + usageFlags, + vk::MemoryPropertyFlagBits::eDeviceLocal, + /*mipLevels*/ + mipLevels, + differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, + families); + resources.textureImage = std::move(textureImg2); + resources.textureImageAllocation = std::move(textureImgAllocation2); + } + + // GPU upload for this texture (copies all regions provided) + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, copyRegions, mipLevels, imageSize); + + // KTX2 files provide their own mip levels; no runtime generation needed + // Store the format and mipLevels for createTextureImageView + resources.format = textureFormat; + resources.mipLevels = mipLevels; + resources.alphaMaskedHint = alphaMaskedHint; + + // Create texture image view + if (!createTextureImageView(resources)) { + return false; + } + + // Create texture sampler + if (!createTextureSampler(resources)) { + return false; + } + + // Add to texture resources map (guarded) + { + std::unique_lock texLock(textureResourcesMutex); + textureResources[textureId] = std::move(resources); + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture image: " << e.what() << std::endl; + return false; + } +} + +// Create texture image view +bool Renderer::createTextureImageView(TextureResources& resources) { + try { + ensureThreadLocalVulkanInit(); + resources.textureImageView = createImageView( + resources.textureImage, + resources.format, + // Use the stored format instead of hardcoded sRGB + vk::ImageAspectFlagBits::eColor, + resources.mipLevels // Use the stored mipLevels + ); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture image view: " << e.what() << std::endl; + return false; + } +} + +// Create shared default PBR textures (to avoid creating hundreds of identical textures) +bool Renderer::createSharedDefaultPBRTextures() { + try { + unsigned char translucentPixel[4] = {128, 128, 128, 128}; // 50% alpha (128/255) + if (!LoadTextureFromMemory(SHARED_DEFAULT_ALBEDO_ID, translucentPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default albedo texture" << std::endl; + return false; + } + + // Create shared default normal texture (flat normal) + unsigned char normalPixel[4] = {128, 128, 255, 255}; // (0.5, 0.5, 1.0, 1.0) in 0-255 range + if (!LoadTextureFromMemory(SHARED_DEFAULT_NORMAL_ID, normalPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default normal texture" << std::endl; + return false; + } + + // Create shared metallic-roughness texture (non-metallic, fully rough) + unsigned char metallicRoughnessPixel[4] = {0, 255, 0, 255}; // (unused, roughness=1.0, metallic=0.0, alpha=1.0) + if (!LoadTextureFromMemory(SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, metallicRoughnessPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default metallic-roughness texture" << std::endl; + return false; + } + + // Create shared default occlusion texture (white - no occlusion) + unsigned char occlusionPixel[4] = {255, 255, 255, 255}; + if (!LoadTextureFromMemory(SHARED_DEFAULT_OCCLUSION_ID, occlusionPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default occlusion texture" << std::endl; + return false; + } + + // Create shared default emissive texture (black - no emission) + unsigned char emissivePixel[4] = {0, 0, 0, 255}; + if (!LoadTextureFromMemory(SHARED_DEFAULT_EMISSIVE_ID, emissivePixel, 1, 1, 4)) { + std::cerr << "Failed to create shared default emissive texture" << std::endl; + return false; + } + + // Create shared bright red texture for ball visibility + unsigned char brightRedPixel[4] = {255, 0, 0, 255}; // Bright red (R=255, G=0, B=0, A=255) + if (!LoadTextureFromMemory(SHARED_BRIGHT_RED_ID, brightRedPixel, 1, 1, 4)) { + std::cerr << "Failed to create shared bright red texture" << std::endl; + return false; + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create shared default PBR textures: " << e.what() << std::endl; + return false; + } +} + +// Create default texture resources (1x1 white texture) +bool Renderer::createDefaultTextureResources() { + try { + // Create a 1x1 white texture + const uint32_t width = 1; + const uint32_t height = 1; + const uint32_t pixelSize = 4; // RGBA + const std::vector pixels = {255, 255, 255, 255}; // White pixel (RGBA) + + // Create staging buffer + vk::DeviceSize imageSize = width * height * pixelSize; + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Copy pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels.data(), static_cast(imageSize)); + stagingBufferMemory.unmapMemory(); + + // Create texture image using memory pool + auto [textureImg, textureImgAllocation] = createImagePooled( + width, + height, + vk::Format::eR8G8B8A8Srgb, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + defaultTextureResources.textureImage = std::move(textureImg); + defaultTextureResources.textureImageAllocation = std::move(textureImgAllocation); + + // Transition image layout for copy + transitionImageLayout( + *defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal); + + // Copy buffer to image + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }; + copyBufferToImage( + *stagingBuffer, + *defaultTextureResources.textureImage, + width, + height, + region); + + // Transition image layout for shader access + transitionImageLayout( + *defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eShaderReadOnlyOptimal); + + // Create texture image view + defaultTextureResources.textureImageView = createImageView( + defaultTextureResources.textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor); + + // Create texture sampler + return createTextureSampler(defaultTextureResources); + } catch (const std::exception& e) { + std::cerr << "Failed to create default texture resources: " << e.what() << std::endl; + return false; + } +} + +// Create texture sampler +bool Renderer::createTextureSampler(TextureResources& resources) { + try { + ensureThreadLocalVulkanInit(); + // Get physical device properties + vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); + + // Create sampler with mipmapping + anisotropy (clamped to device limit) + float deviceMaxAniso = properties.limits.maxSamplerAnisotropy; + float desiredAniso = std::clamp(samplerMaxAnisotropy, 1.0f, deviceMaxAniso); + float maxLod = resources.mipLevels > 1 ? static_cast(resources.mipLevels - 1) : 0.0f; + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .mipLodBias = 0.0f, + .anisotropyEnable = desiredAniso > 1.0f ? VK_TRUE : VK_FALSE, + .maxAnisotropy = desiredAniso, + .compareEnable = VK_FALSE, + .compareOp = vk::CompareOp::eAlways, + .minLod = 0.0f, + .maxLod = maxLod, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = VK_FALSE + }; + + resources.textureSampler = vk::raii::Sampler(device, samplerInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create texture sampler: " << e.what() << std::endl; + return false; + } +} + +// Load texture from file (public wrapper for createTextureImage) +bool Renderer::LoadTexture(const std::string& texturePath) { + ensureThreadLocalVulkanInit(); + if (texturePath.empty()) { + std::cerr << "LoadTexture: Empty texture path provided" << std::endl; + return false; + } + + // Resolve aliases (canonical ID -> actual key) + const std::string resolvedId = ResolveTextureId(texturePath); + + // Check if texture is already loaded + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(resolvedId); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } + } + + // Create temporary texture resources (unused output; cache will be populated internally) + TextureResources tempResources; + + // Use existing createTextureImage method (it inserts into textureResources on success) if it's a KTX2 path; otherwise fall back to memory path below + bool success = false; + if (resolvedId.ends_with(".ktx2")) { + success = createTextureImage(resolvedId, tempResources); + if (success) + return true; + // Fall through to raw-memory path if KTX load failed + } + + if (!success) { + std::cerr << "Failed to load texture: " << texturePath << std::endl; + } + + return success; +} + +// Determine appropriate texture format based on texture type +vk::Format Renderer::determineTextureFormat(const std::string& textureId) { + // Determine sRGB vs Linear in a case-insensitive way + std::string idLower = textureId; + std::ranges::transform(idLower, idLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + // BaseColor/Albedo/Diffuse & SpecGloss RGB should be sRGB for proper gamma correction + if (idLower.find("basecolor") != std::string::npos || + idLower.find("base_color") != std::string::npos || + idLower.find("albedo") != std::string::npos || + idLower.find("diffuse") != std::string::npos || + idLower.find("specgloss") != std::string::npos || + idLower.find("specularglossiness") != std::string::npos || + textureId == Renderer::SHARED_DEFAULT_ALBEDO_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // Emissive is color data and should be sampled in sRGB + if (idLower.find("emissive") != std::string::npos || + textureId == Renderer::SHARED_DEFAULT_EMISSIVE_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // Shared bright red (ball) is a color texture; ensure sRGB for vivid appearance + if (textureId == Renderer::SHARED_BRIGHT_RED_ID) { + return vk::Format::eR8G8B8A8Srgb; + } + + // All other PBR textures (normal, metallic-roughness, occlusion) should be linear + // because they contain non-color data that shouldn't be gamma corrected + return vk::Format::eR8G8B8A8Unorm; +} + +// Load texture from raw image data in memory +bool Renderer::LoadTextureFromMemory(const std::string& textureId, + const unsigned char* imageData, + int width, + int height, + int channels) { + ensureThreadLocalVulkanInit(); + const std::string resolvedId = ResolveTextureId(textureId); + std::cout << "[LoadTextureFromMemory] start id=" << textureId << " -> resolved=" << resolvedId << " size=" << width << "x" << height << " ch=" << channels << std::endl; + if (resolvedId.empty() || !imageData || width <= 0 || height <= 0 || channels <= 0) { + std::cerr << "LoadTextureFromMemory: Invalid parameters" << std::endl; + return false; + } + + // Check if texture is already loaded + { + std::shared_lock texLock(textureResourcesMutex); + auto it = textureResources.find(resolvedId); + if (it != textureResources.end()) { + // Texture already loaded + return true; + } + } + + // Per-texture de-duplication (serialize loads of the same texture ID only) + { + std::unique_lock lk(textureLoadStateMutex); + while (texturesLoading.contains(resolvedId)) { + textureLoadStateCv.wait(lk); + } + } + // Double-check cache after the wait + { + std::shared_lock texLock(textureResourcesMutex); + auto it2 = textureResources.find(resolvedId); + if (it2 != textureResources.end()) { + return true; + } + } + // Mark as loading and ensure we notify on all exit paths + { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.insert(resolvedId); + } + auto _loadingGuard = std::unique_ptr>(reinterpret_cast(1), + [this, resolvedId](void*) { + std::lock_guard lk(textureLoadStateMutex); + texturesLoading.erase(resolvedId); + textureLoadStateCv.notify_all(); + }); + + try { + TextureResources resources; + + // Calculate image size (ensure 4 channels for RGBA) + int targetChannels = 4; // Always use RGBA for consistency + vk::DeviceSize imageSize = width * height * targetChannels; + + // Create a staging buffer + auto [stagingBuffer, stagingBufferMemory] = createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Copy and convert pixel data to staging buffer + void* data = stagingBufferMemory.mapMemory(0, imageSize); + auto* stagingData = static_cast(data); + + if (channels == 4) { + // Already RGBA, direct copy + memcpy(stagingData, imageData, imageSize); + } else if (channels == 3) { + // RGB to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i * 3 + 0]; // R + stagingData[i * 4 + 1] = imageData[i * 3 + 1]; // G + stagingData[i * 4 + 2] = imageData[i * 3 + 2]; // B + stagingData[i * 4 + 3] = 255; // A + } + } else if (channels == 2) { + // Grayscale + Alpha to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i * 2 + 0]; // R (grayscale) + stagingData[i * 4 + 1] = imageData[i * 2 + 0]; // G (grayscale) + stagingData[i * 4 + 2] = imageData[i * 2 + 0]; // B (grayscale) + stagingData[i * 4 + 3] = imageData[i * 2 + 1]; // A (alpha) + } + } else if (channels == 1) { + // Grayscale to RGBA conversion + for (int i = 0; i < width * height; ++i) { + stagingData[i * 4 + 0] = imageData[i]; // R + stagingData[i * 4 + 1] = imageData[i]; // G + stagingData[i * 4 + 2] = imageData[i]; // B + stagingData[i * 4 + 3] = 255; // A + } + } else { + std::cerr << "LoadTextureFromMemory: Unsupported channel count: " << channels << std::endl; + stagingBufferMemory.unmapMemory(); + return false; + } + + // Analyze alpha to set alphaMaskedHint (treat as masked if any pixel alpha < ~1.0) + bool alphaMaskedHint = false; + for (int i = 0, n = width * height; i < n; ++i) { + if (stagingData[i * 4 + 3] < 250) { + alphaMaskedHint = true; + break; + } + } + + stagingBufferMemory.unmapMemory(); + + // Determine the appropriate texture format based on the texture type + vk::Format textureFormat = determineTextureFormat(textureId); + + // Create texture image using memory pool (with optional mipmap generation) + bool differentFamilies = queueFamilyIndices.graphicsFamily.value() != queueFamilyIndices.transferFamily.value(); + std::vector families; + if (differentFamilies) { + families = {queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.transferFamily.value()}; + } + // Decide mip count and usage for memory textures; cap to reduce VRAM pressure + uint32_t mipLevels = 1; + if (width > 1 && height > 1) { + uint32_t full = static_cast(std::floor(std::log2(std::max(width, height)))) + 1; + mipLevels = std::max(1u, std::min(full, maxAutoGeneratedMipLevels)); + } + vk::ImageUsageFlags usageFlags = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + if (mipLevels > 1) + usageFlags |= vk::ImageUsageFlagBits::eTransferSrc; + + // OOM-resilient allocation + try { + auto [textureImg, textureImgAllocation] = createImagePooled( + width, + height, + textureFormat, + vk::ImageTiling::eOptimal, + usageFlags, + vk::MemoryPropertyFlagBits::eDeviceLocal, + mipLevels, + differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, + families); + + resources.textureImage = std::move(textureImg); + resources.textureImageAllocation = std::move(textureImgAllocation); + } catch (const std::exception& e) { + std::cerr << "Image allocation failed (memory texture): " << e.what() << ". Retrying with mipLevels=1..." << std::endl; + mipLevels = 1; + usageFlags &= ~vk::ImageUsageFlagBits::eTransferSrc; + auto [textureImg, textureImgAllocation] = createImagePooled( + width, + height, + textureFormat, + vk::ImageTiling::eOptimal, + usageFlags, + vk::MemoryPropertyFlagBits::eDeviceLocal, + mipLevels, + differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, + families); + resources.textureImage = std::move(textureImg); + resources.textureImageAllocation = std::move(textureImgAllocation); + } + + // GPU upload. Copy buffer to image in a single submit. + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {static_cast(width), static_cast(height), 1} + }; + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, region, mipLevels, imageSize); + + // Generate mip chain if requested and format is uncompressed RGBA + if (mipLevels > 1 && (textureFormat == vk::Format::eR8G8B8A8Srgb || textureFormat == vk::Format::eR8G8B8A8Unorm)) { + generateMipmaps(*resources.textureImage, textureFormat, width, height, mipLevels); + } + + // Store the format for createTextureImageView + resources.format = textureFormat; + resources.mipLevels = mipLevels; + resources.alphaMaskedHint = alphaMaskedHint; + + // Use resolvedId as the cache key to avoid duplicates + const std::string& cacheId = resolvedId; + + // Create texture image view + resources.textureImageView = createImageView( + resources.textureImage, + textureFormat, + vk::ImageAspectFlagBits::eColor, + mipLevels); + + // Create texture sampler + if (!createTextureSampler(resources)) { + return false; + } + + // Add to texture resources map (guarded) + { + std::unique_lock texLock(textureResourcesMutex); + textureResources[cacheId] = std::move(resources); + } + + std::cout << "Successfully loaded texture from memory: " << cacheId + << " (" << width << "x" << height << ", " << channels << " channels)" << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to load texture from memory: " << e.what() << std::endl; + return false; + } +} + +// Create mesh resources +bool Renderer::createMeshResources(MeshComponent* meshComponent, bool deferUpload) { + ensureThreadLocalVulkanInit(); + try { + // If resources already exist, no need to recreate them. + auto it = meshResources.find(meshComponent); + if (it != meshResources.end()) { + // If we previously created this mesh with deferred uploads, but the caller now + // wants an immediate/ready mesh (e.g., during loading or before AS build), + // flush the pending staging copies right here. + if (!deferUpload) { + MeshResources& res = it->second; + if ((res.vertexBufferSizeBytes > 0 && !!*res.stagingVertexBuffer && !!*res.vertexBuffer) || + (res.indexBufferSizeBytes > 0 && !!*res.stagingIndexBuffer && !!*res.indexBuffer)) { + if (res.vertexBufferSizeBytes > 0 && !!*res.stagingVertexBuffer && !!*res.vertexBuffer) { + copyBuffer(res.stagingVertexBuffer, res.vertexBuffer, res.vertexBufferSizeBytes); + res.stagingVertexBuffer = vk::raii::Buffer(nullptr); + res.stagingVertexBufferMemory = vk::raii::DeviceMemory(nullptr); + res.vertexBufferSizeBytes = 0; + } + if (res.indexBufferSizeBytes > 0 && !!*res.stagingIndexBuffer && !!*res.indexBuffer) { + copyBuffer(res.stagingIndexBuffer, res.indexBuffer, res.indexBufferSizeBytes); + res.stagingIndexBuffer = vk::raii::Buffer(nullptr); + res.stagingIndexBufferMemory = vk::raii::DeviceMemory(nullptr); + res.indexBufferSizeBytes = 0; + } + } + } + return true; + } + + // Get mesh data + const auto& vertices = meshComponent->GetVertices(); + const auto& indices = meshComponent->GetIndices(); + + if (vertices.empty() || indices.empty()) { + std::cerr << "Mesh has no vertices or indices" << std::endl; + return false; + } + + // --- 1. Create and fill per-mesh staging buffers on the host --- + vk::DeviceSize vertexBufferSize = sizeof(vertices[0]) * vertices.size(); + auto [stagingVertexBuffer, stagingVertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + void* vertexData = stagingVertexBufferMemory.mapMemory(0, vertexBufferSize); + std::memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); + stagingVertexBufferMemory.unmapMemory(); + + vk::DeviceSize indexBufferSize = sizeof(indices[0]) * indices.size(); + auto [stagingIndexBuffer, stagingIndexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + void* indexData = stagingIndexBufferMemory.mapMemory(0, indexBufferSize); + std::memcpy(indexData, indices.data(), static_cast(indexBufferSize)); + stagingIndexBufferMemory.unmapMemory(); + + // --- 2. Create device-local vertex and index buffers via the memory pool --- + // Add ray tracing flags: eShaderDeviceAddress for vkGetBufferDeviceAddress and + // eAccelerationStructureBuildInputReadOnlyKHR for acceleration structure building + auto [vertexBuffer, vertexBufferAllocation] = createBufferPooled( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer | + vk::BufferUsageFlagBits::eShaderDeviceAddress | vk::BufferUsageFlagBits::eAccelerationStructureBuildInputReadOnlyKHR, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + auto [indexBuffer, indexBufferAllocation] = createBufferPooled( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer | + vk::BufferUsageFlagBits::eShaderDeviceAddress | vk::BufferUsageFlagBits::eAccelerationStructureBuildInputReadOnlyKHR, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // --- 3. Either copy now (legacy path) or defer copies for batched submission --- + MeshResources resources; + resources.vertexBuffer = std::move(vertexBuffer); + resources.vertexBufferAllocation = std::move(vertexBufferAllocation); + resources.indexBuffer = std::move(indexBuffer); + resources.indexBufferAllocation = std::move(indexBufferAllocation); + resources.indexCount = static_cast(indices.size()); + + if (deferUpload) { + // Keep staging buffers alive and record their sizes; copies will be + // performed later by preAllocateEntityResourcesBatch(). + resources.stagingVertexBuffer = std::move(stagingVertexBuffer); + resources.stagingVertexBufferMemory = std::move(stagingVertexBufferMemory); + resources.vertexBufferSizeBytes = vertexBufferSize; + + resources.stagingIndexBuffer = std::move(stagingIndexBuffer); + resources.stagingIndexBufferMemory = std::move(stagingIndexBufferMemory); + resources.indexBufferSizeBytes = indexBufferSize; + } else { + // Immediate upload path used by preAllocateEntityResources() and other + // small-object callers. This preserves existing behaviour. + copyBuffer(stagingVertexBuffer, resources.vertexBuffer, vertexBufferSize); + copyBuffer(stagingIndexBuffer, resources.indexBuffer, indexBufferSize); + // staging* buffers are RAII objects and will be destroyed on scope exit. + } + + // Add to mesh resources map + meshResources[meshComponent] = std::move(resources); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create mesh resources: " << e.what() << std::endl; + return false; + } +} + +// Create uniform buffers +bool Renderer::createUniformBuffers(Entity* entity) { + ensureThreadLocalVulkanInit(); + try { + // Kick watchdog periodically during heavy buffer creation (if called from a long loop) + static uint32_t bufferWatchdogCounter = 0; + if (++bufferWatchdogCounter % 50 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + // Check if entity resources already exist + auto it = entityResources.find(entity); + if (it != entityResources.end()) { + return true; + } + + // Create entity resources + EntityResources resources; + + // Create uniform buffers using memory pool + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + uint32_t numUbos = MAX_FRAMES_IN_FLIGHT; // Multiview uses one UBO per frame for both eyes + for (size_t i = 0; i < numUbos; i++) { + auto [buffer, bufferAllocation] = createBufferPooled( + bufferSize, + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Use the memory pool's mapped pointer if available + void* mappedMemory = bufferAllocation->mappedPtr; + if (!mappedMemory) { + std::cerr << "Warning: Uniform buffer allocation is not mapped" << std::endl; + } + + resources.uniformBuffers.emplace_back(std::move(buffer)); + resources.uniformBufferAllocations.emplace_back(std::move(bufferAllocation)); + resources.uniformBuffersMapped.emplace_back(mappedMemory); + } + + // Initialize descriptor initialization tracking flags to MAX_FRAMES_IN_FLIGHT + resources.pbrUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.basicImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + resources.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + + // Create instance buffer for all entities (shaders always expect instance data) + auto* meshComponent = entity->GetComponent(); + if (meshComponent) { + std::vector instanceData; + + if (meshComponent->GetInstanceCount() > 0) { + // Use existing instance data from GLTF loading (whether 1 or many instances) + instanceData = meshComponent->GetInstances(); + } else { + // Create single instance data using IDENTITY matrix to avoid double-transform with UBO.model + InstanceData singleInstance; + singleInstance.setModelMatrix(glm::mat4(1.0f)); + instanceData = {singleInstance}; + } + + vk::DeviceSize instanceBufferSize = sizeof(InstanceData) * instanceData.size(); + + auto [instanceBuffer, instanceBufferAllocation] = createBufferPooled( + instanceBufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Copy instance data to buffer + void* instanceMappedMemory = instanceBufferAllocation->mappedPtr; + if (instanceMappedMemory) { + std::memcpy(instanceMappedMemory, instanceData.data(), instanceBufferSize); + } else { + std::cerr << "Warning: Instance buffer allocation is not mapped" << std::endl; + } + + resources.instanceBuffer = std::move(instanceBuffer); + resources.instanceBufferAllocation = std::move(instanceBufferAllocation); + resources.instanceBufferMapped = instanceMappedMemory; + } + + // Add to entity resources map + entityResources[entity] = std::move(resources); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create uniform buffers: " << e.what() << std::endl; + return false; + } +} + +// Create descriptor pool +bool Renderer::createDescriptorPool() { + try { + // Calculate pool sizes for all Bistro materials plus additional entities + // The Bistro model creates many more entities than initially expected + // Each entity needs descriptor sets for both basic and PBR pipelines + // PBR pipeline needs 7 descriptors per set (1 UBO + 5 PBR textures + 1 shadow map array with 16 shadow maps) + // Basic pipeline needs 2 descriptors per set (1 UBO + 1 texture) + const uint32_t maxEntities = 20000; // Increased to 20k entities to handle large scenes like Bistro reliably + const uint32_t maxDescriptorSets = MAX_FRAMES_IN_FLIGHT * maxEntities * 2; // 2 pipeline types per entity + + // Calculate descriptor counts + // UBO descriptors: 1 per descriptor set + const uint32_t uboDescriptors = maxDescriptorSets; + // Texture descriptors: Basic pipeline uses 1, PBR uses 21 (5 PBR textures + 16 shadow maps) + // Allocate for worst case: all entities using PBR (21 texture descriptors each) + const uint32_t textureDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 21; + // Storage buffer descriptors: PBR pipeline uses multiple storage buffers per descriptor set. + // Storage buffers used per PBR descriptor set: + // - Binding 6: light storage buffer + // - Binding 7: Forward+ tile headers buffer + // - Binding 8: Forward+ tile indices buffer + // - Binding 9: Fragment debug output buffer (optional) + // - Binding 12: Ray-query geometry info buffer (for raster ray-query shadows) + // - Binding 13: Ray-query material buffer (for raster ray-query shadows) + const uint32_t storageBufferDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 6u; + + // Acceleration structure descriptors: Ray query needs 1 TLAS descriptor per frame + const uint32_t accelerationStructureDescriptors = MAX_FRAMES_IN_FLIGHT; + + // Storage image descriptors: Ray query needs 1 output image descriptor per frame + const uint32_t storageImageDescriptors = MAX_FRAMES_IN_FLIGHT; + + // Reserve extra combined image sampler capacity for Ray Query binding 6 (baseColor texture array) + const uint32_t rqTexDescriptors = MAX_FRAMES_IN_FLIGHT * RQ_MAX_TEX; + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = uboDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = textureDescriptors + rqTexDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eStorageBuffer, + .descriptorCount = storageBufferDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eAccelerationStructureKHR, + .descriptorCount = accelerationStructureDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eStorageImage, + .descriptorCount = storageImageDescriptors + } + }; + + // Create descriptor pool + vk::DescriptorPoolCreateFlags poolFlags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + if (descriptorIndexingEnabled) { + poolFlags |= vk::DescriptorPoolCreateFlagBits::eUpdateAfterBind; + } + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = poolFlags, + .maxSets = maxDescriptorSets, + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data() + }; + + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +// Create descriptor sets +bool Renderer::createDescriptorSets(Entity* entity, const std::string& texturePath, bool usePBR) { + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + return false; + return createDescriptorSets(entity, entityIt->second, texturePath, usePBR); +} + +bool Renderer::createDescriptorSets(Entity* entity, EntityResources& res, const std::string& texturePath, bool usePBR) { + // Kick watchdog periodically during heavy descriptor creation (if called from a long loop) + static uint32_t descWatchdogCounter = 0; + if (++descWatchdogCounter % 50 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + // Resolve alias before taking the shared lock to avoid nested shared_lock on the same mutex + const std::string resolvedTexturePath = ResolveTextureId(texturePath); + try { + vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; + uint32_t numSets = MAX_FRAMES_IN_FLIGHT; // Multiview uses one set per frame for both eyes + std::vector layouts(numSets, selectedLayout); + vk::DescriptorSetAllocateInfo allocInfo{.descriptorPool = *descriptorPool, .descriptorSetCount = numSets, .pSetLayouts = layouts.data()}; + + auto& targetDescriptorSets = usePBR ? res.pbrDescriptorSets : res.basicDescriptorSets; + if (targetDescriptorSets.empty()) { + std::lock_guard lk(descriptorMutex); + // Allocate into a temporary owning container, then move the individual RAII sets into our vector. + // (Avoid assigning `vk::raii::DescriptorSets` directly into `std::vector`.) + auto sets = vk::raii::DescriptorSets(device, allocInfo); + targetDescriptorSets.clear(); + targetDescriptorSets.reserve(sets.size()); + for (auto& s : sets) { + targetDescriptorSets.emplace_back(std::move(s)); + } + } + + // Checking validity prevents SIGSEGV crash when Vulkan tries to access invalid handles. + if (targetDescriptorSets.empty() || targetDescriptorSets.size() < numSets) { + std::cerr << "ERROR: Descriptor set allocation failed for entity " << entity->GetName() + << " (usePBR=" << usePBR << "). Descriptor pool may be exhausted." << std::endl; + return false; + } + + // Only initialize the current frame's descriptor set + // In multiview, both eyes share the same descriptor set for the current frame + { + size_t i = static_cast(currentFrame); + // Validate descriptor set handle before dereferencing to prevent crash + vk::DescriptorSet handleCheck = *targetDescriptorSets[i]; + if (handleCheck == vk::DescriptorSet{}) { + std::cerr << "ERROR: Invalid descriptor set handle for entity " << entity->GetName() + << " frame " << i << " (usePBR=" << usePBR << ")" << std::endl; + return false; + } + vk::DescriptorBufferInfo bufferInfo{.buffer = *res.uniformBuffers[i], .range = sizeof(UniformBufferObject)}; + + if (usePBR) { + // Build descriptor writes dynamically to avoid writing unused bindings + std::vector descriptorWrites; + std::array imageInfos; + // Keep additional descriptor infos alive until updateDescriptorSets completes. + vk::DescriptorImageInfo reflInfo; + vk::WriteDescriptorSetAccelerationStructureKHR tlasInfo{}; + vk::AccelerationStructureKHR tlasHandleValue{}; + vk::DescriptorBufferInfo lightBufferInfo; + vk::DescriptorBufferInfo headersInfo; + vk::DescriptorBufferInfo indicesInfo; + + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}); + + auto meshComponent = entity->GetComponent(); + std::vector pbrTexturePaths; + { + const std::string legacyPath = (meshComponent ? meshComponent->GetTexturePath() : std::string()); + const std::string baseColorPath = (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) ? meshComponent->GetBaseColorTexturePath() : (!legacyPath.empty() ? legacyPath : SHARED_DEFAULT_ALBEDO_ID); + const std::string mrPath = (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + const std::string normalPath = (meshComponent && !meshComponent->GetNormalTexturePath().empty()) ? meshComponent->GetNormalTexturePath() : SHARED_DEFAULT_NORMAL_ID; + const std::string occlusionPath = (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) ? meshComponent->GetOcclusionTexturePath() : SHARED_DEFAULT_OCCLUSION_ID; + const std::string emissivePath = (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) ? meshComponent->GetEmissiveTexturePath() : SHARED_DEFAULT_EMISSIVE_ID; + + pbrTexturePaths = {baseColorPath, mrPath, normalPath, occlusionPath, emissivePath}; + } + + for (int j = 0; j < 5; j++) { + const auto resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); + vk::Sampler samplerHandle{}; + vk::ImageView viewHandle{}; { + std::shared_lock lock(textureResourcesMutex); + auto textureIt = textureResources.find(resolvedBindingPath); + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + samplerHandle = *texRes->textureSampler; + viewHandle = *texRes->textureImageView; + } + imageInfos[j] = {.sampler = samplerHandle, .imageView = viewHandle, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = static_cast(j + 1), .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfos[j]}); + } + + lightBufferInfo = vk::DescriptorBufferInfo{.buffer = *lightStorageBuffers[i % MAX_FRAMES_IN_FLIGHT].buffer, .range = VK_WHOLE_SIZE}; + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightBufferInfo}); + + // Ensure Forward+ per-frame array exists + if (forwardPlusPerFrame.empty()) { + forwardPlusPerFrame.resize(MAX_FRAMES_IN_FLIGHT); + } + + // Ensure tile headers buffer exists (binding 7) - create minimal dummy if needed + if ((i % MAX_FRAMES_IN_FLIGHT) < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[i % MAX_FRAMES_IN_FLIGHT]; + if (!*f.tileHeaders) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; // Single TileHeader {offset, count, pad0, pad1} + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileHeaders = std::move(buf); + f.tileHeadersAlloc = std::move(alloc); + if (!!f.tileHeadersAlloc && f.tileHeadersAlloc->mappedPtr) { + std::memset(f.tileHeadersAlloc->mappedPtr, 0, minSize); + } + } + headersInfo = vk::DescriptorBufferInfo{.buffer = *f.tileHeaders, .offset = 0, .range = VK_WHOLE_SIZE}; + } + + // Ensure tile light indices buffer exists (binding 8) - create minimal dummy if needed + if ((i % MAX_FRAMES_IN_FLIGHT) < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[i % MAX_FRAMES_IN_FLIGHT]; + if (!*f.tileLightIndices) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; // Minimal array of 4 uints + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileLightIndices = std::move(buf); + f.tileLightIndicesAlloc = std::move(alloc); + if (!!f.tileLightIndicesAlloc && f.tileLightIndicesAlloc->mappedPtr) { + std::memset(f.tileLightIndicesAlloc->mappedPtr, 0, minSize); + } + } + indicesInfo = vk::DescriptorBufferInfo{.buffer = *f.tileLightIndices, .offset = 0, .range = VK_WHOLE_SIZE}; + } + + // Now both headersInfo and indicesInfo have valid buffers (never nullptr) + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = 7, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &headersInfo}); + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = 8, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &indicesInfo}); + + // Binding 10: reflection sampler (planar reflections) + // Always bind a safe fallback (default texture) so the descriptor is valid. + reflInfo = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + descriptorWrites.push_back({ + .dstSet = *targetDescriptorSets[i], + .dstBinding = 10, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &reflInfo + }); + + // Binding 11: TLAS (ray-query shadows in raster fragment shader) + // The PBR pipeline layout always declares this binding; it must be written before any draw. + // Bind the current TLAS when AS is enabled. + if (accelerationStructureEnabled) { + vk::AccelerationStructureKHR h = *tlasStructure.handle; + if (!!h) + tlasHandleValue = h; + } + tlasInfo.accelerationStructureCount = 1; + tlasInfo.pAccelerationStructures = &tlasHandleValue; + vk::WriteDescriptorSet tlasWrite{}; + tlasWrite.dstSet = *targetDescriptorSets[i]; + tlasWrite.dstBinding = 11; + tlasWrite.dstArrayElement = 0; + tlasWrite.descriptorCount = 1; + tlasWrite.descriptorType = vk::DescriptorType::eAccelerationStructureKHR; + tlasWrite.pNext = &tlasInfo; + descriptorWrites.push_back(tlasWrite); { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + } else { + // Basic Pipeline + // ... (this part remains the same) + vk::Sampler samplerHandle{}; + vk::ImageView viewHandle{}; { + std::shared_lock lock(textureResourcesMutex); + auto textureIt = textureResources.find(resolvedTexturePath); + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + samplerHandle = *texRes->textureSampler; + viewHandle = *texRes->textureImageView; + } + vk::DescriptorImageInfo imageInfo{.sampler = samplerHandle, .imageView = viewHandle, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + std::array descriptorWrites = { + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[i], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}, + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[i], .dstBinding = 1, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo} + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + } + } + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor sets for " << entity->GetName() << ": " << e.what() << std::endl; + return false; + } +} + +// Pre-allocate all Vulkan resources for an entity during scene loading +bool Renderer::preAllocateEntityResources(Entity* entity) { + try { + // Get the mesh component + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + std::cerr << "Entity " << entity->GetName() << " has no mesh component" << std::endl; + return false; + } + + // Ensure local AABB is available for debug/probes + meshComponent->RecomputeLocalAABB(); + + // 1. Create mesh resources (vertex/index buffers) + if (!createMeshResources(meshComponent)) { + std::cerr << "Failed to create mesh resources for entity: " << entity->GetName() << std::endl; + return false; + } + + // 2. Create uniform buffers + if (!createUniformBuffers(entity)) { + std::cerr << "Failed to create uniform buffers for entity: " << entity->GetName() << std::endl; + return false; + } + + // Initialize per-frame UBO and image binding write flags + { + auto it = entityResources.find(entity); + if (it != entityResources.end()) { + it->second.pbrUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.basicImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + } + + // 3. Pre-allocate BOTH basic and PBR descriptor sets + std::string texturePath = meshComponent->GetTexturePath(); + // Fallback: if legacy texturePath is empty, use PBR baseColor texture + if (texturePath.empty()) { + const std::string& baseColor = meshComponent->GetBaseColorTexturePath(); + if (!baseColor.empty()) { + texturePath = baseColor; + } + } + + // Create basic descriptor sets + if (!createDescriptorSets(entity, texturePath, false)) { + std::cerr << "Failed to create basic descriptor sets for entity: " << entity->GetName() << std::endl; + return false; + } + + // Create PBR descriptor sets + if (!createDescriptorSets(entity, texturePath, true)) { + std::cerr << "Failed to create PBR descriptor sets for entity: " << entity->GetName() << std::endl; + return false; + } + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to pre-allocate resources for entity " << entity->GetName() << ": " << e.what() << std::endl; + return false; + } +} + +// Pre-allocate Vulkan resources for a batch of entities, batching mesh uploads +bool Renderer::preAllocateEntityResourcesBatch(const std::vector& entities) { + watchdogProgressLabel.store("Batch: ensureThreadLocalVulkanInit", std::memory_order_relaxed); + watchdogProgressIndex.store(0, std::memory_order_relaxed); + ensureThreadLocalVulkanInit(); + try { + // --- 1. For all entities, create mesh resources with deferred uploads --- + // Then, during initial loading (and while an AS build is pending), flush the queued + // uploads immediately in a single batched submit (much faster than per-mesh submits). + watchdogProgressLabel.store("Batch: createMeshResources loop", std::memory_order_relaxed); + std::vector meshesNeedingUpload; + meshesNeedingUpload.reserve(entities.size()); + const bool flushUploadsNow = IsLoading() || asBuildRequested.load(std::memory_order_relaxed); + + uint32_t processedMeshes = 0; + uint32_t meshLoopIndex = 0; + for (Entity* entity : entities) { + watchdogProgressIndex.store(meshLoopIndex++, std::memory_order_relaxed); + + if (!entity) { + continue; + } + + // Kick watchdog periodically during heavy mesh resource creation + if (++processedMeshes % 10 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + continue; + } + + // Ensure local AABB is available for debug/probes + watchdogProgressLabel.store("Batch: RecomputeLocalAABB", std::memory_order_relaxed); + meshComponent->RecomputeLocalAABB(); + + watchdogProgressLabel.store("Batch: createMeshResources", std::memory_order_relaxed); + if (!createMeshResources(meshComponent, /*deferUpload=*/true)) { + std::cerr << "Failed to create mesh resources for entity (batch): " + << entity->GetName() << std::endl; + return false; + } + + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) { + continue; + } + MeshResources& res = it->second; + + // Only schedule meshes that still have staged data pending upload + if (res.vertexBufferSizeBytes > 0 || res.indexBufferSizeBytes > 0) { + meshesNeedingUpload.push_back(meshComponent); + } + } + + // --- 2. Defer all GPU copies to the render thread safe point --- + if (!meshesNeedingUpload.empty()) + { + watchdogProgressLabel.store("Batch: EnqueueMeshUploads", std::memory_order_relaxed); + EnqueueMeshUploads(meshesNeedingUpload); + if (flushUploadsNow) { + watchdogProgressLabel.store("Batch: Flush mesh uploads now", std::memory_order_relaxed); + ProcessPendingMeshUploads(); + } + } + + // --- 3. Create uniform buffers and descriptor sets per entity --- + watchdogProgressLabel.store("Batch: per-entity resources loop", std::memory_order_relaxed); + uint32_t processedResources = 0; + uint32_t resourceLoopIndex = 0; + for (Entity* entity : entities) { + watchdogProgressIndex.store(resourceLoopIndex++, std::memory_order_relaxed); + + if (!entity) { + continue; + } + + // Kick watchdog periodically during heavy resource creation + if (++processedResources % 10 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + continue; + } + + watchdogProgressLabel.store("Batch: createUniformBuffers", std::memory_order_relaxed); + if (!createUniformBuffers(entity)) { + std::cerr << "Failed to create uniform buffers for entity (batch): " + << entity->GetName() << std::endl; + return false; + } + + std::string texturePath = meshComponent->GetTexturePath(); + // Fallback: if legacy texturePath is empty, use PBR baseColor texture + if (texturePath.empty()) { + const std::string& baseColor = meshComponent->GetBaseColorTexturePath(); + if (!baseColor.empty()) { + texturePath = baseColor; + } + } + + watchdogProgressLabel.store("Batch: createDescriptorSets (basic)", std::memory_order_relaxed); + if (!createDescriptorSets(entity, texturePath, false)) { + std::cerr << "Failed to create basic descriptor sets for entity (batch): " + << entity->GetName() << std::endl; + return false; + } + + watchdogProgressLabel.store("Batch: createDescriptorSets (pbr)", std::memory_order_relaxed); + if (!createDescriptorSets(entity, texturePath, true)) { + std::cerr << "Failed to create PBR descriptor sets for entity (batch): " + << entity->GetName() << std::endl; + return false; + } + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to batch pre-allocate resources for entities: " << e.what() << std::endl; + return false; + } +} + +// Enqueue a set of meshes to upload on the render thread (safe point) +void Renderer::EnqueueMeshUploads(const std::vector& meshes) { + if (meshes.empty()) + return; + std::lock_guard lk(pendingMeshUploadsMutex); + // Avoid duplicates by using a temporary set of current entries + for (MeshComponent* m : meshes) { + if (!m) + continue; + pendingMeshUploads.push_back(m); + } +} + +void Renderer::EnqueueEntityPreallocationBatch(const std::vector& entities) { + if (entities.empty()) + return; { + std::lock_guard lk(pendingEntityPreallocMutex); + for (Entity* e : entities) { + if (!e) + continue; + pendingEntityPrealloc.push_back(e); + } + } + pendingEntityPreallocQueued.store(true, std::memory_order_relaxed); +} + +void Renderer::EnqueueInstanceBufferRecreation(Entity* entity) { + if (!entity) + return; { + std::lock_guard lk(pendingEntityPreallocMutex); + pendingInstanceBufferRecreations.push_back(entity); + } + pendingEntityPreallocQueued.store(true, std::memory_order_relaxed); +} + +void Renderer::ProcessPendingEntityPreallocations() { + if (!pendingEntityPreallocQueued.load(std::memory_order_relaxed)) + return; + + watchdogProgressLabel.store("Prealloc: drain queues", std::memory_order_relaxed); + + std::vector toPreallocate; + std::vector toRecreateInstances; { + std::lock_guard lk(pendingEntityPreallocMutex); + if (pendingEntityPrealloc.empty() && pendingInstanceBufferRecreations.empty()) { + pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); + return; + } + toPreallocate.swap(pendingEntityPrealloc); + toRecreateInstances.swap(pendingInstanceBufferRecreations); + pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); + } + + // De-dup preallocations + watchdogProgressLabel.store("Prealloc: dedup", std::memory_order_relaxed); + std::sort(toPreallocate.begin(), toPreallocate.end()); + toPreallocate.erase(std::unique(toPreallocate.begin(), toPreallocate.end()), toPreallocate.end()); + + std::vector batch; + batch.reserve(toPreallocate.size()); + for (Entity* e : toPreallocate) { + if (!e || !e->IsActive()) + continue; + if (!e->GetComponent()) + continue; + batch.push_back(e); + } + + if (!batch.empty()) { + watchdogProgressLabel.store("Prealloc: preAllocateEntityResourcesBatch", std::memory_order_relaxed); + if (!preAllocateEntityResourcesBatch(batch)) { + std::cerr << "Warning: batch entity GPU preallocation failed; will retry" << std::endl; + } + } + + // Process instance buffer recreations. + // Wait for GPU idle ONCE before processing the batch to safely destroy old buffers. + if (!toRecreateInstances.empty()) { + watchdogProgressLabel.store("Prealloc: wait other inFlightFences (before recreateInstanceBuffer)", std::memory_order_relaxed); + // IMPORTANT: We are called from the render thread at the frame-start safe point, + // *after* `inFlightFences[currentFrame]` was waited and then reset. + // Waiting on the current frame fence here would deadlock forever because it won't be + // signaled until we submit the current frame (which can't happen while we're blocked). + std::vector fencesToWait; + if (inFlightFences.size() > 1) { + fencesToWait.reserve(inFlightFences.size() - 1); + } + for (uint32_t i = 0; i < static_cast(inFlightFences.size()); ++i) { + if (i == currentFrame) + continue; + if (!!*inFlightFences[i] && *inFlightFences[i] != vk::Fence{}) { + fencesToWait.push_back(*inFlightFences[i]); + } + } + if (!fencesToWait.empty()) { + (void) waitForFencesSafe(fencesToWait, VK_TRUE); + } + watchdogProgressLabel.store("Prealloc: recreateInstanceBuffer loop", std::memory_order_relaxed); + uint32_t processed = 0; + for (Entity* e : toRecreateInstances) { + if (!e || !e->IsActive()) + continue; + + // Kick watchdog periodically during heavy batch processing + if (++processed % 10 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + if (!recreateInstanceBuffer(e)) { + std::cerr << "Warning: failed to recreate instance buffer for entity " << e->GetName() << std::endl; + } + } + } + + watchdogProgressLabel.store("Prealloc: done", std::memory_order_relaxed); +} + +// Execute pending mesh uploads on the render thread after the per-frame fence wait +void Renderer::ProcessPendingMeshUploads() { + // 0. Retire completed async upload batches (if timeline semaphore is available) + if (!!*uploadsTimeline && *uploadsTimeline != vk::Semaphore{}) { + uint64_t completedValue = 0; + try { + // vk::raii::Device doesn't expose getSemaphoreCounterValue in all Vulkan-Hpp versions; + // use the underlying vk::Device handle. + completedValue = (*device).getSemaphoreCounterValue(*uploadsTimeline); + } catch (...) { + completedValue = 0; + } + + bool anyCompleted = false; + while (true) { + InFlightMeshUploadBatch completedBatch; { + std::lock_guard lk(inFlightMeshUploadsMutex); + if (inFlightMeshUploads.empty()) + break; + if (inFlightMeshUploads.front().signalValue == 0 || inFlightMeshUploads.front().signalValue > completedValue) + break; + completedBatch = std::move(inFlightMeshUploads.front()); + inFlightMeshUploads.pop_front(); + } + + // Clear staging once copies are complete + for (auto* meshComponent : completedBatch.meshes) { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources& res = it->second; + res.stagingVertexBuffer = vk::raii::Buffer(nullptr); + res.stagingVertexBufferMemory = vk::raii::DeviceMemory(nullptr); + res.vertexBufferSizeBytes = 0; + res.stagingIndexBuffer = vk::raii::Buffer(nullptr); + res.stagingIndexBufferMemory = vk::raii::DeviceMemory(nullptr); + res.indexBufferSizeBytes = 0; + } + + anyCompleted = true; + } + + if (anyCompleted) { + // Now that more meshes are READY (uploads finished), request a TLAS rebuild so + // non‑instanced and previously missing meshes are included in the acceleration structure. + asDevOverrideAllowRebuild = true; // allow rebuild even if frozen + RequestAccelerationStructureBuild("uploads completed"); + } + } + + // Grab the list atomically + std::vector toProcess; { + std::lock_guard lk(pendingMeshUploadsMutex); + if (pendingMeshUploads.empty()) + return; + toProcess.swap(pendingMeshUploads); + } + + // Build a quick lookup of meshes already in flight so we don't submit duplicate copies + std::unordered_set inFlightMeshes; { + std::lock_guard lk(inFlightMeshUploadsMutex); + for (const auto& b : inFlightMeshUploads) { + for (auto* m : b.meshes) { + inFlightMeshes.insert(m); + } + } + } + + // Filter to meshes that still have staged data + std::vector needsCopy; + needsCopy.reserve(toProcess.size()); + for (auto* meshComponent : toProcess) { + if (inFlightMeshes.find(meshComponent) != inFlightMeshes.end()) + continue; + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + const MeshResources& res = it->second; + if (res.vertexBufferSizeBytes > 0 || res.indexBufferSizeBytes > 0) { + needsCopy.push_back(meshComponent); + } + } + if (needsCopy.empty()) + return; + + // Record copies on GRAPHICS queue to avoid cross-queue hazards while stabilizing + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + + // Prefer async submission via the uploads timeline semaphore to avoid blocking the render thread. + // However, during initial loading (and when an AS build is pending), we want mesh uploads to + // complete promptly so readiness can increase and the AS can be built within the target budget. + const bool forceSynchronous = IsLoading() || asBuildRequested.load(std::memory_order_relaxed); + const bool canSignalTimeline = (!!*uploadsTimeline && *uploadsTimeline != vk::Semaphore{}) && !forceSynchronous; + if (canSignalTimeline) { + auto tempPool = std::make_unique(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = **tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + auto cbs = std::make_unique(device, allocInfo); + vk::raii::CommandBuffer& cb = (*cbs)[0]; + cb.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + for (auto* meshComponent : needsCopy) { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources& res = it->second; + if (res.vertexBufferSizeBytes > 0 && !!*res.stagingVertexBuffer && !!*res.vertexBuffer) { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; + cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + } + if (res.indexBufferSizeBytes > 0 && !!*res.stagingIndexBuffer && !!*res.indexBuffer) { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; + cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + } + } + + cb.end(); + + uint64_t signalValue = 0; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submitInfo.pNext = &timelineInfo; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &*cb; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &*uploadsTimeline; + graphicsQueue.submit(submitInfo, vk::Fence{}); + } + + InFlightMeshUploadBatch batch; + batch.signalValue = signalValue; + batch.meshes = std::move(needsCopy); + batch.commandPool = std::move(tempPool); + batch.commandBuffers = std::move(cbs); { + std::lock_guard lk(inFlightMeshUploadsMutex); + inFlightMeshUploads.push_back(std::move(batch)); + } + } else { + // Fallback: submit and wait on the GRAPHICS queue (single-threaded via queueMutex) + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + cb.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + for (auto* meshComponent : needsCopy) { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources& res = it->second; + if (res.vertexBufferSizeBytes > 0 && !!*res.stagingVertexBuffer && !!*res.vertexBuffer) { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; + cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + } + if (res.indexBufferSizeBytes > 0 && !!*res.stagingIndexBuffer && !!*res.indexBuffer) { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; + cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + } + } + + cb.end(); + + vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*cb}; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + + for (auto* meshComponent : needsCopy) { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources& res = it->second; + res.stagingVertexBuffer.clear(); + res.stagingVertexBufferMemory.clear(); + res.vertexBufferSizeBytes = 0; + res.stagingIndexBuffer.clear(); + res.stagingIndexBufferMemory.clear(); + res.indexBufferSizeBytes = 0; + } + + asDevOverrideAllowRebuild = true; + RequestAccelerationStructureBuild("uploads completed"); + } +} + +// Recreate instance buffer for an entity (e.g., after clearing instances for animation) +bool Renderer::recreateInstanceBuffer(Entity* entity) { + ensureThreadLocalVulkanInit(); + try { + // Find the entity in entityResources + auto it = entityResources.find(entity); + if (it == entityResources.end()) { + std::cerr << "Entity " << entity->GetName() << " not found in entityResources" << std::endl; + return false; + } + + EntityResources& resources = it->second; + + // Create a single instance with identity matrix + InstanceData singleInstance; + singleInstance.setModelMatrix(glm::mat4(1.0f)); + std::vector instanceData = {singleInstance}; + + vk::DeviceSize instanceBufferSize = sizeof(InstanceData) * instanceData.size(); + + // Create new instance buffer using memory pool + auto [instanceBuffer, instanceBufferAllocation] = createBufferPooled( + instanceBufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Copy instance data to buffer + void* instanceMappedMemory = instanceBufferAllocation->mappedPtr; + if (instanceMappedMemory) { + std::memcpy(instanceMappedMemory, instanceData.data(), instanceBufferSize); + } else { + std::cerr << "Warning: Instance buffer allocation is not mapped" << std::endl; + } + + // Replace the old instance buffer with the new one. + // Note: Caller must ensure GPU is idle before this method is called to safely destroy the old buffer. + resources.instanceBuffer = std::move(instanceBuffer); + resources.instanceBufferAllocation = std::move(instanceBufferAllocation); + resources.instanceBufferMapped = instanceMappedMemory; + + std::cout << "[Animation] Recreated instance buffer for entity '" << entity->GetName() + << "' with single identity instance" << std::endl; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to recreate instance buffer for entity " << entity->GetName() + << ": " << e.what() << std::endl; + return false; + } +} + +// Create buffer using memory pool for efficient allocation +std::pair> Renderer::createBufferPooled( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + if (!memoryPool) { + throw std::runtime_error("Memory pool not initialized"); + } + + // Use memory pool for allocation + auto [buffer, allocation] = memoryPool->createBuffer(size, usage, properties); + + return {std::move(buffer), std::move(allocation)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create buffer with memory pool: " << e.what() << std::endl; + throw; + } +} + +// Legacy createBuffer function - now strictly enforces memory pool usage +std::pair Renderer::createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + // This function should only be used for temporary staging buffers during resource creation + // All persistent resources should use createBufferPooled directly + + if (!memoryPool) { + throw std::runtime_error("Memory pool not available - cannot create buffer"); + } + + // Only allow direct allocation for staging buffers (temporary, host-visible) + if (!(properties & vk::MemoryPropertyFlagBits::eHostVisible)) { + std::cerr << "ERROR: Legacy createBuffer should only be used for staging buffers!" << std::endl; + throw std::runtime_error("Legacy createBuffer used for non-staging buffer"); + } + + try { + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device, bufferInfo); + + // Allocate memory directly for staging buffers only + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + // Align allocation size to nonCoherentAtomSize (64 bytes) to prevent validation errors + // VUID-VkMappedMemoryRange-size-01390 requires memory flush sizes to be multiples of nonCoherentAtomSize + const vk::DeviceSize nonCoherentAtomSize = 64; // Typical value, should query from device properties + vk::DeviceSize alignedSize = ((memRequirements.size + nonCoherentAtomSize - 1) / nonCoherentAtomSize) * nonCoherentAtomSize; + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = alignedSize, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory bufferMemory(device, allocInfo); + + // Bind memory to buffer + buffer.bindMemory(*bufferMemory, 0); + + return {std::move(buffer), std::move(bufferMemory)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create staging buffer: " << e.what() << std::endl; + throw; + } +} + +void Renderer::createTransparentDescriptorSets() { + // We need one descriptor set per frame in flight for this resource + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *transparentDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; { + // Serialize allocation vs other descriptor ops + std::lock_guard lk(descriptorMutex); + transparentDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + } + + // Update each descriptor set to point to the per-frame off-screen opaque color image + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DescriptorImageInfo imageInfo{ + .sampler = *opaqueSceneColorSampler, + .imageView = *opaqueSceneColorImageViews[i], + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *transparentDescriptorSets[i], + .dstBinding = 0, // Binding 0 in Set 1 + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrite, nullptr); + } + } +} + +void Renderer::createTransparentFallbackDescriptorSets() { + // Allocate one descriptor set per frame in flight using the same layout (single combined image sampler at binding 0) + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *transparentDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; { + std::lock_guard lk(descriptorMutex); + transparentFallbackDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + } + + // Point each set to the default texture, which is guaranteed to be in SHADER_READ_ONLY_OPTIMAL when used in the opaque pass + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DescriptorImageInfo imageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *transparentFallbackDescriptorSets[i], + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrite, nullptr); + } + } +} + +bool Renderer::createOpaqueSceneColorResources() { + try { + opaqueSceneColorImages.clear(); + opaqueSceneColorImageAllocations.clear(); + opaqueSceneColorImageViews.clear(); + opaqueSceneColorImageLayouts.clear(); + + opaqueSceneColorImages.reserve(MAX_FRAMES_IN_FLIGHT); + opaqueSceneColorImageAllocations.reserve(MAX_FRAMES_IN_FLIGHT); + opaqueSceneColorImageViews.reserve(MAX_FRAMES_IN_FLIGHT); + opaqueSceneColorImageLayouts.reserve(MAX_FRAMES_IN_FLIGHT); + + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + auto [image, allocation] = createImagePooled( + swapChainExtent.width, + swapChainExtent.height, + swapChainImageFormat, + // Use the same format as the swapchain + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + opaqueSceneColorImages.push_back(std::move(image)); + opaqueSceneColorImageAllocations.push_back(std::move(allocation)); + opaqueSceneColorImageViews.push_back(createImageView(opaqueSceneColorImages.back(), swapChainImageFormat, vk::ImageAspectFlagBits::eColor)); + opaqueSceneColorImageLayouts.push_back(vk::ImageLayout::eUndefined); + } + + // Create (or recreate) the sampler (shared across frames) + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, + }; + opaqueSceneColorSampler = vk::raii::Sampler(device, samplerInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create opaque scene color resources: " << e.what() << std::endl; + return false; + } +} + +// Copy buffer +void Renderer::copyBuffer(vk::raii::Buffer& srcBuffer, vk::raii::Buffer& dstBuffer, vk::DeviceSize size) { + ensureThreadLocalVulkanInit(); + try { + // Create a temporary transient command pool and command buffer to isolate per-thread usage (transfer family) + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Copy buffer + vk::BufferCopy copyRegion{ + .srcOffset = 0, + .dstOffset = 0, + .size = size + }; + + commandBuffer.copyBuffer(*srcBuffer, *dstBuffer, copyRegion); + + // End command buffer + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + // Use mutex to ensure thread-safe access to transfer queue + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); { + std::lock_guard lock(queueMutex); + transferQueue.submit(submitInfo, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + } catch (const std::exception& e) { + std::cerr << "Failed to copy buffer: " << e.what() << std::endl; + throw; + } +} + +// Create image +std::pair Renderer::createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties, + uint32_t arrayLayers) { + try { + // Create image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = arrayLayers, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + vk::raii::Image image(device, imageInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory imageMemory(device, allocInfo); + + // Bind memory to image + image.bindMemory(*imageMemory, 0); + + return {std::move(image), std::move(imageMemory)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create image: " << e.what() << std::endl; + throw; + } +} + +// Create image using memory pool for efficient allocation +std::pair> Renderer::createImagePooled( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties, + uint32_t mipLevels, + vk::SharingMode sharingMode, + const std::vector& queueFamilies) { + try { + if (!memoryPool) { + throw std::runtime_error("Memory pool not initialized"); + } + + // Use memory pool for allocation (mipmap support limited by memory pool API) + auto [image, allocation] = memoryPool->createImage(width, + height, + format, + tiling, + usage, + properties, + mipLevels, + sharingMode, + queueFamilies); + + return {std::move(image), std::move(allocation)}; + } catch (const std::exception& e) { + std::cerr << "Failed to create image with memory pool: " << e.what() << std::endl; + throw; + } +} + +// Create an image view +vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels, uint32_t layerCount) { + try { + ensureThreadLocalVulkanInit(); + // Create image view + vk::ImageViewCreateInfo viewInfo{ + .image = *image, + .viewType = (layerCount > 1) ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = layerCount + } + }; + + return {device, viewInfo}; + } catch (const std::exception& e) { + std::cerr << "Failed to create image view: " << e.what() << std::endl; + throw; + } +} + +// Transition image layout +void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels, uint32_t layerCount) { + ensureThreadLocalVulkanInit(); + try { + // Create a temporary transient command pool and command buffer to isolate per-thread usage + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Create an image barrier (Sync2) + vk::ImageMemoryBarrier2 barrier2{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = layerCount + } + }; + + // Set stage and access masks based on layouts + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier2.dstAccessMask = vk::AccessFlagBits2::eTransferWrite; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier2.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; + barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead | vk::AccessFlagBits2::eDepthStencilAttachmentWrite; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) { + // Support for shadow map creation: transition from undefined to read-only depth layout + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; + barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eGeneral) { + // Support for compute shader storage images: transition from undefined to general layout + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderWrite | vk::AccessFlagBits2::eShaderRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + // Support for textures that skip staging buffer (e.g., preloaded, generated, or default textures) + // Transition directly from undefined to shader read-only for sampling + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + } else { + throw std::invalid_argument("Unsupported layout transition!"); + } + + // Add a barrier to command buffer (Sync2) + vk::DependencyInfo depInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier2 + }; + commandBuffer.pipelineBarrier2(depInfo); + std::cout << "[transitionImageLayout] recorded barrier image=" << (void *) image << " old=" << static_cast(oldLayout) << " new=" << static_cast(newLayout) << std::endl; + + // End command buffer + commandBuffer.end(); + + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = !!*uploadsTimeline; + uint64_t signalValue = 0; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + if (canSignalTimeline) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submitInfo.pNext = &timelineInfo; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &*uploadsTimeline; + } + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &*commandBuffer; + graphicsQueue.submit(submitInfo, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + } catch (const std::exception& e) { + std::cerr << "Failed to transition image layout: " << e.what() << std::endl; + throw; + } +} + +// Copy buffer to image +void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, vk::ArrayProxy regions) { + ensureThreadLocalVulkanInit(); + try { + // Create a temporary transient command pool for the GRAPHICS queue to avoid cross-queue races + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffers commandBuffers(device, allocInfo); + vk::raii::CommandBuffer& commandBuffer = commandBuffers[0]; + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + // Copy buffer to image using provided regions + commandBuffer.copyBufferToImage( + buffer, + image, + vk::ImageLayout::eTransferDstOptimal, + regions); + std::cout << "[copyBufferToImage] recorded copy img=" << (void *) image << std::endl; + + // End command buffer + commandBuffer.end(); + + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = !!*uploadsTimeline; + uint64_t signalValue = 0; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + if (canSignalTimeline) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submitInfo.pNext = &timelineInfo; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &*uploadsTimeline; + } + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &*commandBuffer; + graphicsQueue.submit(submitInfo, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + } catch (const std::exception& e) { + std::cerr << "Failed to copy buffer to image: " << e.what() << std::endl; + throw; + } +} + +// Create or resize light storage buffers to accommodate the given number of lights +bool Renderer::createOrResizeLightStorageBuffers(size_t lightCount) { + try { + // Ensure we have storage buffers for each frame in flight + if (lightStorageBuffers.size() != MAX_FRAMES_IN_FLIGHT) { + lightStorageBuffers.resize(MAX_FRAMES_IN_FLIGHT); + } + + // Check if we need to resize buffers + bool needsResize = false; + for (auto& buffer : lightStorageBuffers) { + if (buffer.capacity < lightCount) { + needsResize = true; + break; + } + } + + if (!needsResize) { + return true; // Buffers are already large enough + } + + // Calculate new capacity (with some headroom for growth) + size_t newCapacity = std::max(lightCount * 2, static_cast(64)); + vk::DeviceSize bufferSize = sizeof(LightData) * newCapacity; + + // Wait for device to be idle before destroying old buffers to prevent validation errors. + // External synchronization required (VVL): serialize against queue submits/present. + WaitIdle(); + + // Create new buffers for each frame + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + auto& buffer = lightStorageBuffers[i]; + + // Clean up old buffer if it exists (now safe after waitIdle) + if (!!buffer.allocation) { + buffer.buffer = vk::raii::Buffer(nullptr); + buffer.allocation.reset(); + buffer.mapped = nullptr; + } + + // Create new storage buffer + auto [newBuffer, newAllocation] = createBufferPooled( + bufferSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Get the mapped pointer from the allocation + void* mapped = newAllocation->mappedPtr; + + // Store the new buffer + buffer.buffer = std::move(newBuffer); + buffer.allocation = std::move(newAllocation); + buffer.mapped = mapped; + buffer.capacity = newCapacity; + buffer.size = 0; + } + + // Update all existing descriptor sets to reference the new light storage buffers + updateAllDescriptorSetsWithNewLightBuffers(); + + // Also refresh Forward+ compute descriptor sets (binding 0) so compute reads valid buffers + try { + if (!forwardPlusPerFrame.empty()) { + for (size_t i = 0; i < forwardPlusPerFrame.size() && i < lightStorageBuffers.size(); ++i) { + if (!*forwardPlusPerFrame[i].computeSet) + continue; + if (!*lightStorageBuffers[i].buffer) + continue; + vk::DescriptorBufferInfo lightsInfo{.buffer = *lightStorageBuffers[i].buffer, .offset = 0, .range = VK_WHOLE_SIZE}; + vk::WriteDescriptorSet write{ + .dstSet = *forwardPlusPerFrame[i].computeSet, + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightsInfo + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(write, {}); + } + } + } + } catch (const std::exception& e) { + std::cerr << "Failed to update Forward+ compute descriptors after light buffer resize: " << e.what() << std::endl; + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create or resize light storage buffers: " << e.what() << std::endl; + return false; + } +} + +// Update all existing descriptor sets with new light storage buffer references +void Renderer::updateAllDescriptorSetsWithNewLightBuffers(bool allFrames) { + try { + if (!descriptorSetsValid.load(std::memory_order_relaxed)) + return; + if (isRecordingCmd.load(std::memory_order_relaxed)) + return; + // Iterate through all entity resources and update their PBR descriptor sets + for (auto& kv : entityResources) { + auto& resources = kv.second; + // Only update PBR descriptor sets (they have light buffer bindings) + if (!resources.pbrDescriptorSets.empty()) { + size_t beginFrame = allFrames ? 0 : static_cast(currentFrame); + size_t endFrame = allFrames ? resources.pbrDescriptorSets.size() : (beginFrame + 1); + for (size_t i = beginFrame; i < endFrame && i < resources.pbrDescriptorSets.size() && i < lightStorageBuffers.size(); ++i) { + // Skip if this set looks invalid/uninitialized + if (!(*resources.pbrDescriptorSets[i])) + continue; + if (i < lightStorageBuffers.size() && !!*lightStorageBuffers[i].buffer) { + // Create descriptor write for light storage buffer (binding 7) + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *resources.pbrDescriptorSets[i], + .dstBinding = 6, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + + // Update the descriptor set + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrite, {}); + } + } + } + } + } + } catch (const std::exception& e) { + std::cerr << "Failed to update descriptor sets with new light buffers: " << e.what() << std::endl; + } +} + +// Refresh only current frame's PBR descriptor bindings used by Forward+ +// Safe to call after waiting on inFlightFences[currentFrame] and before command recording. +void Renderer::refreshPBRForwardPlusBindingsForFrame(uint32_t frameIndex) { + try { + if (frameIndex >= MAX_FRAMES_IN_FLIGHT) + return; + if (!descriptorSetsValid.load(std::memory_order_relaxed)) + return; + if (isRecordingCmd.load(std::memory_order_relaxed)) + return; + + // Resolve current frame Forward+ buffers + vk::Buffer headersBuf{}; + vk::Buffer indicesBuf{}; + if (frameIndex < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[frameIndex]; + if (!!*f.tileHeaders) + headersBuf = *f.tileHeaders; + if (!!*f.tileLightIndices) + indicesBuf = *f.tileLightIndices; + } + + // Resolve current frame lights buffer + vk::Buffer lightsBuf{}; + if (frameIndex < lightStorageBuffers.size() && !!*lightStorageBuffers[frameIndex].buffer) { + lightsBuf = *lightStorageBuffers[frameIndex].buffer; + } + + // Ensure lights buffer exists (binding 6) - create minimal dummy if needed + if (!lightsBuf) { + // Lazily create a minimal lights buffer (single LightData element) for use when Forward+ is disabled + if (lightStorageBuffers.empty()) { + lightStorageBuffers.resize(MAX_FRAMES_IN_FLIGHT); + } + if (frameIndex < lightStorageBuffers.size() && !*lightStorageBuffers[frameIndex].buffer) { + vk::DeviceSize minSize = sizeof(LightData); // Single light element + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + lightStorageBuffers[frameIndex].buffer = std::move(buf); + lightStorageBuffers[frameIndex].allocation = std::move(alloc); + lightStorageBuffers[frameIndex].mapped = lightStorageBuffers[frameIndex].allocation->mappedPtr; + lightStorageBuffers[frameIndex].capacity = 1; + lightStorageBuffers[frameIndex].size = 0; + // Zero-initialize to prevent garbage data + if (!!lightStorageBuffers[frameIndex].mapped) { + std::memset(lightStorageBuffers[frameIndex].mapped, 0, minSize); + } + } + if (frameIndex < lightStorageBuffers.size() && !!*lightStorageBuffers[frameIndex].buffer) { + lightsBuf = *lightStorageBuffers[frameIndex].buffer; + } + } + + // Ensure tile headers buffer exists (binding 7) - create minimal dummy if needed + if (!headersBuf) { + if (forwardPlusPerFrame.empty()) { + forwardPlusPerFrame.resize(MAX_FRAMES_IN_FLIGHT); + } + if (frameIndex < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[frameIndex]; + if (!*f.tileHeaders) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; // Single TileHeader {offset, count, pad0, pad1} + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileHeaders = std::move(buf); + f.tileHeadersAlloc = std::move(alloc); + if (!!f.tileHeadersAlloc && f.tileHeadersAlloc->mappedPtr) { + std::memset(f.tileHeadersAlloc->mappedPtr, 0, minSize); + } + } + if (!!*f.tileHeaders) + headersBuf = *f.tileHeaders; + } + } + + // Ensure tile light indices buffer exists (binding 8) - create minimal dummy if needed + if (!indicesBuf) { + if (forwardPlusPerFrame.empty()) { + forwardPlusPerFrame.resize(MAX_FRAMES_IN_FLIGHT); + } + if (frameIndex < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[frameIndex]; + if (!*f.tileLightIndices) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; // Minimal array of 4 uints + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileLightIndices = std::move(buf); + f.tileLightIndicesAlloc = std::move(alloc); + if (!!f.tileLightIndicesAlloc && f.tileLightIndicesAlloc->mappedPtr) { + std::memset(f.tileLightIndicesAlloc->mappedPtr, 0, minSize); + } + } + if (!!*f.tileLightIndices) + indicesBuf = *f.tileLightIndices; + } + } + + std::vector writes; + vk::DescriptorBufferInfo lightsInfo{}; + vk::DescriptorBufferInfo headersInfo{}; + vk::DescriptorBufferInfo indicesInfo{}; + vk::DescriptorBufferInfo geoInfoInfo{}; + vk::DescriptorBufferInfo matInfoInfo{}; + vk::DescriptorBufferInfo fragDbgInfo{}; + + // At this point, all three critical buffers (lights, headers, indices) should exist (real or dummy) + if (!!lightsBuf) { + lightsInfo = vk::DescriptorBufferInfo{.buffer = lightsBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + } + // Current frame fragment debug buffer (reuse compute debugOut) - this one is optional + if (frameIndex < forwardPlusPerFrame.size()) { + auto& fpf = forwardPlusPerFrame[frameIndex]; + if (!!*fpf.debugOut) { + fragDbgInfo = vk::DescriptorBufferInfo{.buffer = *fpf.debugOut, .offset = 0, .range = VK_WHOLE_SIZE}; + } + } + if (!!headersBuf) { + headersInfo = vk::DescriptorBufferInfo{.buffer = headersBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + } + if (!!indicesBuf) { + indicesInfo = vk::DescriptorBufferInfo{.buffer = indicesBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + } + + // Binding 10: reflection sampler — always bind fallback texture while reflection pass is disabled + // The reflection rendering pass is currently disabled (commented out in renderer_rendering.cpp + // lines 1194-1203), so we must not bind any reflection RTs that may exist but contain stale data. + // When reflection rendering is re-enabled, restore the conditional logic to bind previous frame's RT. + vk::DescriptorImageInfo reflInfo{}; + reflInfo = vk::DescriptorImageInfo{.sampler = *defaultTextureResources.textureSampler, .imageView = *defaultTextureResources.textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + + // Binding 11: TLAS (for raster ray-query shadows) + // Raster PBR shaders can statically declare/use `tlas` even when ray-query mode is disabled, + // so the descriptor must be written whenever acceleration structures are enabled. + vk::AccelerationStructureKHR tlasHandleValue = accelerationStructureEnabled ? *tlasStructure.handle : vk::AccelerationStructureKHR{}; + vk::WriteDescriptorSetAccelerationStructureKHR tlasInfo{}; + tlasInfo.accelerationStructureCount = 1; + tlasInfo.pAccelerationStructures = &tlasHandleValue; + + for (auto& kv : entityResources) { + auto& res = kv.second; + if (res.pbrDescriptorSets.empty() || frameIndex >= res.pbrDescriptorSets.size()) + continue; + + // This prevents "Invalid VkDescriptorSet Object" errors when sets have been freed/invalidated + if (!(*res.pbrDescriptorSets[frameIndex])) { + std::cerr << "Warning: Invalid descriptor set handle for entity at frame " << frameIndex << ", skipping" << std::endl; + continue; + } + + // Binding 6: lights SSBO - ALWAYS bind (required by layout) + if (!!lightsBuf) { + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightsInfo}); + } + // Binding 7: tile headers - ALWAYS bind (required by layout) + if (!!headersBuf) { + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 7, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &headersInfo}); + } + // Binding 8: tile indices - ALWAYS bind (required by layout) + if (!!indicesBuf) { + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 8, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &indicesInfo}); + } + // Binding 9: fragment debug output buffer (optional - only bind if exists) + if (!!fragDbgInfo.buffer) { + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 9, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &fragDbgInfo}); + } + // Binding 10: reflection sampler - ALWAYS bind (required by layout) + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 10, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &reflInfo}); + + // Binding 11: TLAS - ALWAYS bind (required by layout when ray query/AS is enabled) + // If TLAS is not built yet, the handle will be null; the shader must not trace when disabled. + vk::WriteDescriptorSet tlasWrite{}; + tlasWrite.dstSet = *res.pbrDescriptorSets[frameIndex]; + tlasWrite.dstBinding = 11; + tlasWrite.dstArrayElement = 0; + tlasWrite.descriptorCount = 1; + tlasWrite.descriptorType = vk::DescriptorType::eAccelerationStructureKHR; + tlasWrite.pNext = &tlasInfo; + writes.push_back(tlasWrite); + + // Binding 12/13: Ray-query geometry/material buffers for material-aware raster shadow queries. + // Always bind something valid; shader guards on `ubo.geometryInfoCount/materialCount`. + vk::Buffer fallbackBuf = headersBuf ? headersBuf : indicesBuf; + vk::Buffer geoBuf = (!!*geometryInfoBuffer) ? *geometryInfoBuffer : fallbackBuf; + vk::Buffer matBuf = (!!*materialBuffer) ? *materialBuffer : fallbackBuf; + geoInfoInfo = vk::DescriptorBufferInfo{.buffer = geoBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + matInfoInfo = vk::DescriptorBufferInfo{.buffer = matBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 12, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &geoInfoInfo}); + writes.push_back(vk::WriteDescriptorSet{.dstSet = *res.pbrDescriptorSets[frameIndex], .dstBinding = 13, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &matInfoInfo}); + } + + if (!writes.empty()) { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + } catch (const std::exception& e) { + std::cerr << "Failed to refresh PBR Forward+ bindings for frame " << frameIndex << ": " << e.what() << std::endl; + } +} + +// Update the light storage buffer with current light data +bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector& lights, CameraComponent* camera) { + try { + // Ensure buffers are large enough and properly initialized + if (!createOrResizeLightStorageBuffers(lights.size())) { + return false; + } + + // Now check frame index after buffers are properly initialized + if (frameIndex >= lightStorageBuffers.size()) { + std::cerr << "Invalid frame index for light storage buffer update: " << frameIndex + << " >= " << lightStorageBuffers.size() << std::endl; + return false; + } + + auto& buffer = lightStorageBuffers[frameIndex]; + if (!buffer.mapped) { + std::cerr << "Light storage buffer not mapped" << std::endl; + return false; + } + + // Convert ExtractedLight data to LightData format + auto* lightData = static_cast(buffer.mapped); + for (size_t i = 0; i < lights.size(); ++i) { + const auto& light = lights[i]; + + // For directional lights, store direction in position field (they don't need position) + // For other lights, store position + if (light.type == ExtractedLight::Type::Directional) { + lightData[i].position = glm::vec4(light.direction, 0.0f); // w=0 indicates direction + } else { + lightData[i].position = glm::vec4(light.position, 1.0f); // w=1 indicates position + } + + lightData[i].color = glm::vec4(light.color * light.intensity, 1.0f); + lightData[i].direction = glm::vec4(light.direction, 0.0f); + + // Calculate light space matrix for shadow mapping + glm::mat4 lightProjection, lightView; + if (light.type == ExtractedLight::Type::Directional) { + float orthoSize = 50.0f; + glm::vec3 shadowCamPos = light.position; + glm::vec3 lightDir = glm::normalize(light.direction); + if (camera) { + // Center shadow map on camera frustum + glm::vec3 camPos = camera->GetPosition(); + shadowCamPos = camPos - lightDir * 50.0f; + } + lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, 0.1f, 200.0f); + + // Robust up vector to avoid LookAt singularities with vertical lights + glm::vec3 up = (std::abs(lightDir.y) > 0.99f) ? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f); + lightView = glm::lookAt(shadowCamPos, shadowCamPos + lightDir, up); + } else { + lightProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, light.range); + lightView = glm::lookAt(light.position, light.position + light.direction, glm::vec3(0.0f, 1.0f, 0.0f)); + } + lightData[i].lightSpaceMatrix = lightProjection * lightView; + + // Set light type + switch (light.type) { + case ExtractedLight::Type::Point: + lightData[i].lightType = 0; + break; + case ExtractedLight::Type::Directional: + lightData[i].lightType = 1; + break; + case ExtractedLight::Type::Spot: + lightData[i].lightType = 2; + break; + case ExtractedLight::Type::Emissive: + lightData[i].lightType = 3; + break; + } + + // Set other light properties + lightData[i].range = light.range; + lightData[i].innerConeAngle = light.innerConeAngle; + lightData[i].outerConeAngle = light.outerConeAngle; + } + + // Update buffer size + buffer.size = lights.size(); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to update light storage buffer: " << e.what() << std::endl; + return false; + } +} + +// Asynchronous texture loading implementations using ThreadPool +std::future Renderer::LoadTextureAsync(const std::string& texturePath, bool critical) { + if (texturePath.empty()) { + return std::async(std::launch::deferred, [] { return false; }); + } + // Schedule a CPU-light job that enqueues a pending GPU upload to be + // processed later on the main thread. This avoids submitting Vulkan + // command buffers from worker threads, which can confuse GPU-assisted + // validation. + textureTasksScheduled.fetch_add(1, std::memory_order_relaxed); + uploadJobsTotal.fetch_add(1, std::memory_order_relaxed); + auto task = [this, texturePath, critical]() { + PendingTextureJob job; + job.type = PendingTextureJob::Type::FromFile; + job.priority = critical ? PendingTextureJob::Priority::Critical : PendingTextureJob::Priority::NonCritical; + job.idOrPath = texturePath; { + std::lock_guard lk(pendingTextureJobsMutex); + pendingTextureJobs.emplace_back(std::move(job)); + } + pendingTextureCv.notify_one(); + if (critical) { + criticalJobsOutstanding.fetch_add(1, std::memory_order_relaxed); + } + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return true; + }; + + std::shared_lock lock(threadPoolMutex); + if (!threadPool) { + return std::async(std::launch::async, task); + } + return threadPool->enqueue(task); +} + +std::future Renderer::LoadTextureFromMemoryAsync(const std::string& textureId, + const unsigned char* imageData, + int width, + int height, + int channels, + bool critical) { + if (!imageData || textureId.empty() || width <= 0 || height <= 0 || channels <= 0) { + return std::async(std::launch::deferred, [] { return false; }); + } + // Copy the source bytes so the caller can free/modify their buffer immediately + size_t srcSize = static_cast(width) * static_cast(height) * static_cast(channels); + std::vector dataCopy(srcSize); + std::memcpy(dataCopy.data(), imageData, srcSize); + + textureTasksScheduled.fetch_add(1, std::memory_order_relaxed); + uploadJobsTotal.fetch_add(1, std::memory_order_relaxed); + auto task = [this, textureId, data = std::move(dataCopy), width, height, channels, critical]() mutable { + PendingTextureJob job; + job.type = PendingTextureJob::Type::FromMemory; + job.priority = critical ? PendingTextureJob::Priority::Critical : PendingTextureJob::Priority::NonCritical; + job.idOrPath = textureId; { + std::lock_guard lk(pendingTextureJobsMutex); + pendingTextureJobs.emplace_back(std::move(job)); + } + pendingTextureCv.notify_one(); + if (critical) { + criticalJobsOutstanding.fetch_add(1, std::memory_order_relaxed); + } + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return true; + }; + + std::shared_lock lock(threadPoolMutex); + if (!threadPool) { + return std::async(std::launch::async, std::move(task)); + } + return threadPool->enqueue(std::move(task)); +} + +void Renderer::WaitForAllTextureTasks() { + // Simple blocking wait: spin until all scheduled texture tasks have completed. + // This is only intended for use during initial scene loading where a short + // stall is acceptable to ensure descriptor sets see all real textures. + for (;;) { + uint32_t scheduled = textureTasksScheduled.load(std::memory_order_relaxed); + uint32_t completed = textureTasksCompleted.load(std::memory_order_relaxed); + if (scheduled == 0 || completed >= scheduled) { + break; + } + // Sleep briefly to yield CPU while background texture jobs finish + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +// Start background worker threads that drain pending texture jobs and perform GPU uploads +void Renderer::StartUploadsWorker(size_t workerCount) { + stopUploadsWorker.store(false, std::memory_order_relaxed); + if (workerCount == 0) { + unsigned int hw = std::thread::hardware_concurrency(); + // Heuristic: at least 2 workers, at most 4, and not exceeding half of HW threads + unsigned int target = std::max(2u, std::min(4u, hw > 0 ? hw / 2 : 2u)); + workerCount = static_cast(target); + } + uploadsWorkerThreads.reserve(workerCount); + for (size_t t = 0; t < workerCount; ++t) { + uploadsWorkerThreads.emplace_back([this]() { + ensureThreadLocalVulkanInit(); + while (!stopUploadsWorker.load(std::memory_order_relaxed)) { + // Wait for work or stop signal + { + std::unique_lock lk(pendingTextureJobsMutex); + pendingTextureCv.wait(lk, + [this]() { + return stopUploadsWorker.load(std::memory_order_relaxed) || !pendingTextureJobs.empty(); + }); + } + if (stopUploadsWorker.load(std::memory_order_relaxed)) + break; + + // Drain a batch of jobs + std::vector batch; { + std::lock_guard lk(pendingTextureJobsMutex); + const size_t maxBatch = 16; // simple batch size to limit command overhead + const size_t take = std::min(maxBatch, pendingTextureJobs.size()); + batch.reserve(take); + for (size_t i = 0; i < take; ++i) { + batch.emplace_back(std::move(pendingTextureJobs.back())); + pendingTextureJobs.pop_back(); + } + } + + // Process critical jobs first + auto isCritical = [](const PendingTextureJob& j) { return j.priority == PendingTextureJob::Priority::Critical; }; + std::stable_sort(batch.begin(), + batch.end(), + [&](const PendingTextureJob& a, const PendingTextureJob& b) { + return isCritical(a) && !isCritical(b); + }); + + // Try to batch FromMemory jobs together for a single transfer submit + std::vector memJobs; + for (auto& j : batch) + if (j.type == PendingTextureJob::Type::FromMemory) + memJobs.push_back(std::move(j)); + // Remove moved jobs from batch + batch.erase(std::remove_if(batch.begin(), batch.end(), [](const PendingTextureJob& j) { return j.type == PendingTextureJob::Type::FromMemory; }), batch.end()); + + if (!memJobs.empty()) { + try { + // Process batched memory uploads with a single submit + // Fallback to per-job if batching fails for any reason + auto processSingle = [&](const PendingTextureJob& job) { + (void) LoadTextureFromMemory(job.idOrPath, + job.data.data(), + job.width, + job.height, + job.channels); + OnTextureUploaded(job.idOrPath); + if (job.priority == PendingTextureJob::Priority::Critical) { + criticalJobsOutstanding.fetch_sub(1, std::memory_order_relaxed); + } + uploadJobsCompleted.fetch_add(1, std::memory_order_relaxed); + }; + + // Build staging buffers and images without submitting yet + struct Item { + std::string id; + vk::raii::Buffer staging; + std::unique_ptr stagingAlloc; + std::vector tmp; + uint32_t w, h; + vk::Format format; + std::vector regions; + uint32_t mipLevels; + vk::raii::Image image; + std::unique_ptr imageAlloc; + }; + std::vector items; + items.reserve(memJobs.size()); + + for (auto& job : memJobs) { + try { + // Create staging buffer and copy data + const vk::DeviceSize imgSize = static_cast(job.width * job.height * 4); + auto [stagingBuf, stagingAlloc] = createBufferPooled(imgSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + void* mapped = stagingAlloc->mappedPtr; + // Convert to RGBA if not already + std::vector rgba; + rgba.resize(static_cast(imgSize)); + const uint8_t* src = job.data.data(); + if (job.channels == 4) { + std::memcpy(rgba.data(), src, static_cast(imgSize)); + } else if (job.channels == 3) { + for (int y = 0; y < job.height; ++y) { + for (int x = 0; x < job.width; ++x) { + size_t si = (y * job.width + x) * 3; + size_t di = (y * job.width + x) * 4; + rgba[di + 0] = src[si + 0]; + rgba[di + 1] = src[si + 1]; + rgba[di + 2] = src[si + 2]; + rgba[di + 3] = 255; + } + } + } else if (job.channels == 1) { + for (int i = 0, n = job.width * job.height; i < n; ++i) { + uint8_t v = src[i]; + size_t di = i * 4; + rgba[di + 0] = v; + rgba[di + 1] = v; + rgba[di + 2] = v; + rgba[di + 3] = 255; + } + } else { + // unsupported layout, fallback to single path which will handle it + processSingle(job); + continue; + } + std::memcpy(mapped, rgba.data(), static_cast(imgSize)); + // Persistent mapping via memory pool; no explicit unmap needed here + + // Create image (concurrent sharing if needed) + bool differentFamilies = queueFamilyIndices.graphicsFamily.value() != queueFamilyIndices.transferFamily.value(); + std::vector families; + if (differentFamilies) + families = {queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.transferFamily.value()}; + vk::Format texFormat = determineTextureFormat(job.idOrPath); + auto [image, imageAlloc] = createImagePooled(job.width, job.height, texFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); + + // Prepare one region + std::vector regions{ + vk::BufferImageCopy{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {.aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1}, + .imageOffset = {0, 0, 0}, + .imageExtent = {static_cast(job.width), static_cast(job.height), 1} + } + }; + + items.push_back(Item{job.idOrPath, std::move(stagingBuf), std::move(stagingAlloc), std::move(rgba), static_cast(job.width), static_cast(job.height), texFormat, std::move(regions), 1, std::move(image), std::move(imageAlloc)}); + } catch (const std::exception& e) { + std::cerr << "Batch prepare failed for '" << job.idOrPath << "': " << e.what() << ". Falling back to single." << std::endl; + processSingle(job); + continue; + } + } + + if (!items.empty()) { + // Record a single command buffer for all items + vk::CommandPoolCreateInfo poolInfo{.flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueFamilyIndices.transferFamily.value()}; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{.commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + cb.begin(vk::CommandBufferBeginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + for (auto& it : items) { + // Transition undefined->transfer dst (Sync2) + vk::ImageMemoryBarrier2 toDst2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *it.image, + .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1} + }; + vk::DependencyInfo depToDst{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &toDst2}; + cb.pipelineBarrier2(depToDst); + + cb.copyBufferToImage(*it.staging, *it.image, vk::ImageLayout::eTransferDstOptimal, it.regions); + + // Transition to shader-read (Sync2) + vk::ImageMemoryBarrier2 toShader2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *it.image, + .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1} + }; + vk::DependencyInfo depToShader{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &toShader2}; + cb.pipelineBarrier2(depToShader); + } + + cb.end(); + + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + uint64_t signalValue = 0; + bool canSignal = !!*uploadsTimeline; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + if (canSignal) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + transferQueue.submit(submit, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + + // Perf accounting for the batch + uint64_t batchBytes = 0; + for (auto& it : items) + batchBytes += static_cast(it.w) * it.h * 4ull; + bytesUploadedTotal.fetch_add(batchBytes, std::memory_order_relaxed); + uploadCount.fetch_add(static_cast(items.size()), std::memory_order_relaxed); + + // Finalize resources and notify + for (auto& it : items) { + // Store in textureResources + TextureResources res; + res.textureImage = std::move(it.image); + res.textureImageAllocation = std::move(it.imageAlloc); + res.format = it.format; + res.mipLevels = it.mipLevels; + res.alphaMaskedHint = false; // heuristic omitted in batch + // Create sampler/view + createTextureSampler(res); + res.textureImageView = createImageView(res.textureImage, res.format, vk::ImageAspectFlagBits::eColor, res.mipLevels); { + std::unique_lock lk(textureResourcesMutex); + textureResources[it.id] = std::move(res); + } + OnTextureUploaded(it.id); + // Update counters + uploadJobsCompleted.fetch_add(1, std::memory_order_relaxed); + } + // Decrement outstanding critical jobs if any + for (auto& job : memJobs) + if (job.priority == PendingTextureJob::Priority::Critical) + criticalJobsOutstanding.fetch_sub(1, std::memory_order_relaxed); + } + } catch (const std::exception& e) { + std::cerr << "UploadsWorker: batch processing failed: " << e.what() << std::endl; + // Fallback: per-job processing + for (auto& job : memJobs) { + try { + (void) LoadTextureFromMemory(job.idOrPath, + job.data.data(), + job.width, + job.height, + job.channels); + OnTextureUploaded(job.idOrPath); + if (job.priority == PendingTextureJob::Priority::Critical) { + criticalJobsOutstanding.fetch_sub(1, std::memory_order_relaxed); + } + uploadJobsCompleted.fetch_add(1, std::memory_order_relaxed); + } catch (...) { + } + } + } + } + + // Process remaining non-memory jobs individually + for (auto& job : batch) { + try { + if (job.type == PendingTextureJob::Type::FromFile) { + (void) LoadTexture(job.idOrPath); + OnTextureUploaded(job.idOrPath); + if (job.priority == PendingTextureJob::Priority::Critical) { + criticalJobsOutstanding.fetch_sub(1, std::memory_order_relaxed); + } + uploadJobsCompleted.fetch_add(1, std::memory_order_relaxed); + } + } catch (const std::exception& e) { + std::cerr << "UploadsWorker: failed to process job for '" << job.idOrPath << "': " << e.what() << std::endl; + } + } + } + }); + } +} + +void Renderer::StopUploadsWorker() { + stopUploadsWorker.store(true, std::memory_order_relaxed); + pendingTextureCv.notify_all(); + for (auto& th : uploadsWorkerThreads) { + if (th.joinable()) + th.join(); + } + uploadsWorkerThreads.clear(); +} + +void Renderer::RegisterTextureUser(const std::string& textureId, Entity* entity) { + if (textureId.empty() || !entity) + return; + + // Always register under the canonical resolved ID so that lookups from + // descriptor creation and upload completion (which also use + // ResolveTextureId) are consistent. + std::string canonicalId = ResolveTextureId(textureId); + if (canonicalId.empty()) { + canonicalId = textureId; + } + + std::lock_guard lk(textureUsersMutex); + textureToEntities[canonicalId].push_back(entity); +} + +void Renderer::OnTextureUploaded(const std::string& textureId) { + // Resolve alias to canonical ID used for tracking and descriptor + // creation. RegisterTextureUser also stores under this canonical ID. + std::string canonicalId = ResolveTextureId(textureId); + if (canonicalId.empty()) { + canonicalId = textureId; + } + + std::vector users; { + std::lock_guard lk(textureUsersMutex); + auto it = textureToEntities.find(canonicalId); + if (it == textureToEntities.end()) { + return; + } + users = it->second; + } + + // Always defer descriptor updates to the safe point at the start of Render() + // (after the in-flight fence for the current frame has been signaled). + // This avoids UPDATE_AFTER_BIND violations and mid-recording invalidation. + // If descriptor indexing / UPDATE_AFTER_BIND is enabled, we still prefer + // this safer path for consistency across devices. + for (Entity* entity : users) { + if (!entity) + continue; + MarkEntityDescriptorsDirty(entity); + } + + // Ray Query uses a global texture table (binding 6) that may reference this texture. + // Mark the ray query descriptor sets dirty for all frames so the render-thread safe point + // can refresh the table when the texture becomes available. + if (rayQueryEnabled && accelerationStructureEnabled) { + const uint32_t allFramesMask = (MAX_FRAMES_IN_FLIGHT >= 32u) ? 0xFFFFFFFFu : ((1u << MAX_FRAMES_IN_FLIGHT) - 1u); + rayQueryDescriptorsDirtyMask.fetch_or(allFramesMask, std::memory_order_relaxed); + } +} + +void Renderer::MarkEntityDescriptorsDirty(Entity* entity) { + if (!entity) + return; + // Mark this entity as needing refresh for *all* frames-in-flight. + // Each frame will refresh its own descriptor sets at its safe point. + const uint32_t allFramesMask = (MAX_FRAMES_IN_FLIGHT >= 32u) ? 0xFFFFFFFFu : ((1u << MAX_FRAMES_IN_FLIGHT) - 1u); + std::lock_guard lk(dirtyEntitiesMutex); + auto& mask = descriptorDirtyEntities[entity]; + mask |= allFramesMask; +} + +bool Renderer::updateDescriptorSetsForFrame(Entity* entity, + const std::string& texturePath, + bool usePBR, + uint32_t frameIndex, + bool imagesOnly, + bool uboOnly) { + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + return false; + return updateDescriptorSetsForFrame(entity, entityIt->second, texturePath, usePBR, frameIndex, imagesOnly, uboOnly); +} + +bool Renderer::updateDescriptorSetsForFrame(Entity* entity, + EntityResources& res, + const std::string& texturePath, + bool usePBR, + uint32_t frameIndex, + bool imagesOnly, + bool uboOnly) { + if (!entity) + return false; + if (!descriptorSetsValid.load(std::memory_order_relaxed)) { + // Descriptor sets are being recreated; skip updates for now + return false; + } + // Defer descriptor writes if the command buffer is currently being recorded. + if (isRecordingCmd.load(std::memory_order_relaxed)) { + std::lock_guard qlk(pendingDescMutex); + pendingDescOps.push_back(PendingDescOp{entity, texturePath, usePBR, frameIndex, imagesOnly}); + descriptorRefreshPending.store(true, std::memory_order_relaxed); + return true; + } + // IMPORTANT: Do NOT hold `textureResourcesMutex` across this function. + // We may call `ResolveTextureId()` (which also locks it), and `std::shared_mutex` is not recursive. + + // Ensure we have a valid UBO for this frame before attempting descriptor writes + if (frameIndex >= res.uniformBuffers.size() || + frameIndex >= res.uniformBuffersMapped.size() || + *res.uniformBuffers[frameIndex] == vk::Buffer{}) { + // Missing UBO for this frame; skip to avoid writing invalid descriptors + return false; + } + + vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; + // Ensure descriptor sets exist for this entity + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); + vk::DescriptorSetAllocateInfo allocInfo{.descriptorPool = *descriptorPool, .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, .pSetLayouts = layouts.data()}; + auto& targetDescriptorSets = usePBR ? res.pbrDescriptorSets : res.basicDescriptorSets; + bool newlyAllocated = false; + if (targetDescriptorSets.empty()) { + std::lock_guard lk(descriptorMutex); + targetDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + newlyAllocated = true; + } + if (frameIndex >= targetDescriptorSets.size()) + return false; + + vk::DescriptorBufferInfo bufferInfo{.buffer = *res.uniformBuffers[frameIndex], .range = sizeof(UniformBufferObject)}; + + // Ensure per-pipeline UBO init tracking is sized + if (res.pbrUboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) { + res.pbrUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (res.basicUboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) { + res.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (res.pbrFixedBindingsWritten.size() != MAX_FRAMES_IN_FLIGHT) { + res.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (res.pbrImagesWritten.size() != MAX_FRAMES_IN_FLIGHT) { + res.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (res.basicImagesWritten.size() != MAX_FRAMES_IN_FLIGHT) { + res.basicImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + + if (usePBR) { + // We'll fill descriptor writes. Binding 0 (UBO) is written only when explicitly requested (uboOnly) + // or when doing a full update (imagesOnly == false). For imagesOnly updates we must NOT touch UBO + // to avoid update-after-bind hazards. + std::vector writes; + std::array imageInfos; + // Helper: ensure required PBR layout bindings (7/8/10/11) are written at least once per frame. + // IMPORTANT: descriptor infos must remain alive until `updateDescriptorSets` is called. + vk::DescriptorBufferInfo headersInfo{}; + vk::DescriptorBufferInfo indicesInfo{}; + vk::DescriptorBufferInfo geoInfoInfo{}; + vk::DescriptorBufferInfo matInfoInfo{}; + vk::DescriptorImageInfo reflInfo{}; + vk::AccelerationStructureKHR tlasHandleValue{}; + vk::WriteDescriptorSetAccelerationStructureKHR tlasInfo{}; + vk::WriteDescriptorSet tlasWrite{}; + const bool needFixedWrites = !res.pbrFixedBindingsWritten[frameIndex]; + auto appendPbrFixedWrites = [&](std::vector& dstWrites) { + if (!needFixedWrites) + return; + + // Binding 7/8: Forward+ tile buffers (must be valid even when Forward+ is disabled) + if (forwardPlusPerFrame.empty()) { + forwardPlusPerFrame.resize(MAX_FRAMES_IN_FLIGHT); + } + vk::Buffer headersBuf{}; + vk::Buffer indicesBuf{}; + if (frameIndex < forwardPlusPerFrame.size()) { + auto& f = forwardPlusPerFrame[frameIndex]; + if (!!*f.tileHeaders) + headersBuf = *f.tileHeaders; + if (!!*f.tileLightIndices) + indicesBuf = *f.tileLightIndices; + if (!headersBuf) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; // Single TileHeader + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileHeaders = std::move(buf); + f.tileHeadersAlloc = std::move(alloc); + if (!!f.tileHeadersAlloc && f.tileHeadersAlloc->mappedPtr) { + std::memset(f.tileHeadersAlloc->mappedPtr, 0, minSize); + } + headersBuf = *f.tileHeaders; + } + if (!indicesBuf) { + vk::DeviceSize minSize = sizeof(uint32_t) * 4; + auto [buf, alloc] = createBufferPooled(minSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileLightIndices = std::move(buf); + f.tileLightIndicesAlloc = std::move(alloc); + if (!!f.tileLightIndicesAlloc && f.tileLightIndicesAlloc->mappedPtr) { + std::memset(f.tileLightIndicesAlloc->mappedPtr, 0, minSize); + } + indicesBuf = *f.tileLightIndices; + } + } + headersInfo = vk::DescriptorBufferInfo{.buffer = headersBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + indicesInfo = vk::DescriptorBufferInfo{.buffer = indicesBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + dstWrites.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 7, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &headersInfo}); + dstWrites.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 8, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &indicesInfo}); + + // Binding 10: reflection sampler (always bind safe fallback) + reflInfo = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + dstWrites.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 10, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &reflInfo}); + + // Binding 11: TLAS (ray-query shadows in raster PBR fragment shader) + tlasHandleValue = accelerationStructureEnabled ? *tlasStructure.handle : vk::AccelerationStructureKHR{}; + tlasInfo.accelerationStructureCount = 1; + tlasInfo.pAccelerationStructures = &tlasHandleValue; + tlasWrite.dstSet = *targetDescriptorSets[frameIndex]; + tlasWrite.dstBinding = 11; + tlasWrite.dstArrayElement = 0; + tlasWrite.descriptorCount = 1; + tlasWrite.descriptorType = vk::DescriptorType::eAccelerationStructureKHR; + tlasWrite.pNext = &tlasInfo; + dstWrites.push_back(tlasWrite); + + // Binding 12/13: Ray-query geometry/material buffers for material-aware raster shadow queries. + // Always bind something valid; shader guards on `ubo.geometryInfoCount/materialCount`. + vk::Buffer fallbackBuf = headersBuf ? headersBuf : indicesBuf; + vk::Buffer geoBuf = (!!*geometryInfoBuffer) ? *geometryInfoBuffer : fallbackBuf; + vk::Buffer matBuf = (!!*materialBuffer) ? *materialBuffer : fallbackBuf; + geoInfoInfo = vk::DescriptorBufferInfo{.buffer = geoBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + matInfoInfo = vk::DescriptorBufferInfo{.buffer = matBuf, .offset = 0, .range = VK_WHOLE_SIZE}; + dstWrites.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 12, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &geoInfoInfo}); + dstWrites.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 13, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &matInfoInfo}); + }; + + // Optionally write only the UBO (binding 0) — used at safe point to initialize per-frame sets once + if (uboOnly) { + // Avoid re-writing if we already initialized this frame's UBO binding + if (!res.pbrUboBindingWritten[frameIndex]) { + writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}); + } + appendPbrFixedWrites(writes); + if (!writes.empty()) { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + if (!res.pbrUboBindingWritten[frameIndex]) { + res.pbrUboBindingWritten[frameIndex] = true; + } + if (needFixedWrites) { + res.pbrFixedBindingsWritten[frameIndex] = true; + } + } + return true; + } + + // For full updates (imagesOnly == false), include UBO write; for imagesOnly, skip it + if (!imagesOnly) { + writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}); + } + + auto meshComponent = entity->GetComponent(); + // Determine PBR texture paths in the same manner as createDescriptorSets + std::string legacyPath = (meshComponent ? meshComponent->GetTexturePath() : std::string()); + const std::string baseColorPath = (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) ? meshComponent->GetBaseColorTexturePath() : (!legacyPath.empty() ? legacyPath : SHARED_DEFAULT_ALBEDO_ID); + const std::string mrPath = (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID; + const std::string normalPath = (meshComponent && !meshComponent->GetNormalTexturePath().empty()) ? meshComponent->GetNormalTexturePath() : SHARED_DEFAULT_NORMAL_ID; + const std::string occlusionPath = (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) ? meshComponent->GetOcclusionTexturePath() : SHARED_DEFAULT_OCCLUSION_ID; + const std::string emissivePath = (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) ? meshComponent->GetEmissiveTexturePath() : SHARED_DEFAULT_EMISSIVE_ID; + std::array pbrTexturePaths = {baseColorPath, mrPath, normalPath, occlusionPath, emissivePath}; + + for (int j = 0; j < 5; ++j) { + const std::string resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); + vk::Sampler samplerHandle{}; + vk::ImageView viewHandle{}; { + std::shared_lock lock(textureResourcesMutex); + auto textureIt = textureResources.find(resolvedBindingPath); + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + samplerHandle = *texRes->textureSampler; + viewHandle = *texRes->textureImageView; + } + imageInfos[j] = {.sampler = samplerHandle, .imageView = viewHandle, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = static_cast(j + 1), .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfos[j]}); + } + // Ensure Forward+ light buffer (binding 6) is written for the current frame when available. + // Do this even on imagesOnly updates so set 0 is fully valid for PBR shading. + if (frameIndex < lightStorageBuffers.size() && !!*lightStorageBuffers[frameIndex].buffer) { + vk::DescriptorBufferInfo lightBufferInfo{.buffer = *lightStorageBuffers[frameIndex].buffer, .range = VK_WHOLE_SIZE}; + writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightBufferInfo}); + } + appendPbrFixedWrites(writes); { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + if (needFixedWrites) { + res.pbrFixedBindingsWritten[frameIndex] = true; + } + if (!imagesOnly) { + res.pbrUboBindingWritten[frameIndex] = true; + } + } else { + const std::string resolvedTexturePath = ResolveTextureId(texturePath); + vk::Sampler samplerHandle{}; + vk::ImageView viewHandle{}; { + std::shared_lock lock(textureResourcesMutex); + auto textureIt = textureResources.find(resolvedTexturePath); + TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + samplerHandle = *texRes->textureSampler; + viewHandle = *texRes->textureImageView; + } + vk::DescriptorImageInfo imageInfo{.sampler = samplerHandle, .imageView = viewHandle, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + if (imagesOnly && !newlyAllocated) { + std::array descriptorWrites = { + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 1, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo} + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + } else { + // If uboOnly is requested for basic pipeline, only write binding 0 + if (uboOnly) { + if (!res.basicUboBindingWritten[frameIndex]) { + std::array descriptorWrites = { + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo} + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + res.basicUboBindingWritten[frameIndex] = true; + } + return true; + } + std::array descriptorWrites = { + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}, + vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 1, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo} + }; { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + res.basicUboBindingWritten[frameIndex] = true; + } + } + return true; +} + +void Renderer::ProcessDirtyDescriptorsForFrame(uint32_t frameIndex) { + if (frameIndex >= 32u) + return; + const uint32_t frameBit = (1u << frameIndex); + + std::vector toProcess; { + std::lock_guard lk(dirtyEntitiesMutex); + if (descriptorDirtyEntities.empty()) + return; + toProcess.reserve(descriptorDirtyEntities.size()); + for (auto& [e, mask] : descriptorDirtyEntities) { + if (!!e && (mask & frameBit)) { + toProcess.push_back(e); + } + } + } + + uint32_t processed = 0; + for (Entity* entity : toProcess) { + if (!entity) + continue; + + // Kick watchdog periodically during heavy descriptor processing + if (++processed % 10 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + // Resolve a texture path to pass for the basic pipeline + std::string basicTexPath = meshComponent->GetTexturePath(); + if (basicTexPath.empty()) + basicTexPath = meshComponent->GetBaseColorTexturePath(); + // Update strategy: + // - Only update the current frame here at the safe point. + // Other frames will be updated at their own safe points to avoid UPDATE_AFTER_BIND violations. + auto entityIt = entityResources.find(entity); + if (entityIt != entityResources.end()) { + updateDescriptorSetsForFrame(entity, entityIt->second, basicTexPath, false, frameIndex, /*imagesOnly=*/true); + updateDescriptorSetsForFrame(entity, entityIt->second, basicTexPath, true, frameIndex, /*imagesOnly=*/true); + } + // Do not touch descriptors for other frames while their command buffers may be pending. + } + + // Clear the processed bit; keep entities dirty until all frames have been refreshed. + { + std::lock_guard lk(dirtyEntitiesMutex); + for (Entity* entity : toProcess) { + auto it = descriptorDirtyEntities.find(entity); + if (it == descriptorDirtyEntities.end()) + continue; + it->second &= ~frameBit; + if (it->second == 0u) { + descriptorDirtyEntities.erase(it); + } + } + } +} + +void Renderer::ProcessPendingTextureJobs(uint32_t maxJobs, + bool includeCritical, + bool includeNonCritical) { + // If the background uploads worker is running, it will handle draining + // texture jobs. Keep this function as a safe no-op for render-thread code + // paths that still call it. + if (!uploadsWorkerThreads.empty() && !stopUploadsWorker.load(std::memory_order_relaxed)) { + return; + } + // Drain the pending job list under lock into a local vector, then + // perform a bounded number of texture loads (including Vulkan work) + // on this thread. This must be called from the main/render thread. + std::vector jobs; { + std::lock_guard lk(pendingTextureJobsMutex); + if (pendingTextureJobs.empty()) { + return; + } + jobs.swap(pendingTextureJobs); + } + + std::vector remaining; + remaining.reserve(jobs.size()); + + uint32_t processed = 0; + uint32_t watchdogCounter = 0; + for (auto& job : jobs) { + // Kick watchdog periodically during heavy texture processing + if (++watchdogCounter % 10 == 0) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + const bool isCritical = (job.priority == PendingTextureJob::Priority::Critical); + if (processed < maxJobs && + ((isCritical && includeCritical) || (!isCritical && includeNonCritical))) { + switch (job.type) { + case PendingTextureJob::Type::FromFile: + // LoadTexture will resolve aliases and perform full GPU upload + LoadTexture(job.idOrPath); + break; + case PendingTextureJob::Type::FromMemory: + // LoadTextureFromMemory will create GPU resources for this ID + LoadTextureFromMemory(job.idOrPath, + job.data.data(), + job.width, + job.height, + job.channels); + break; + } + // Refresh descriptors for entities that use this texture so + // streaming uploads become visible in the scene. + OnTextureUploaded(job.idOrPath); + if (isCritical) { + criticalJobsOutstanding.fetch_sub(1, std::memory_order_relaxed); + } + uploadJobsCompleted.fetch_add(1, std::memory_order_relaxed); + ++processed; + } else { + remaining.emplace_back(std::move(job)); + } + } + + if (!remaining.empty()) { + std::lock_guard lk(pendingTextureJobsMutex); + // Append remaining jobs back to the pending queue + pendingTextureJobs.insert(pendingTextureJobs.end(), + std::make_move_iterator(remaining.begin()), + std::make_move_iterator(remaining.end())); + } +} + +// Record both layout transitions and the copy in a single submission with a fence +void Renderer::uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + vk::ArrayProxy regions, + uint32_t mipLevels, + vk::DeviceSize stagedBytes) { + ensureThreadLocalVulkanInit(); + try { + // Start perf window on first upload + if (uploadWindowStartNs.load(std::memory_order_relaxed) == 0) { + auto now = std::chrono::steady_clock::now().time_since_epoch(); + uint64_t nowNs = static_cast(std::chrono::duration_cast(now).count()); + uploadWindowStartNs.store(nowNs, std::memory_order_relaxed); + } + auto t0 = std::chrono::steady_clock::now(); + + // Use a temporary transient command pool for the TRANSFER queue family + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.transferFamily.value() + }; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *tempPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + + vk::CommandBufferBeginInfo beginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}; + cb.begin(beginInfo); + + // Barrier: Undefined -> TransferDstOptimal (all mip levels that will be copied) (Sync2) + vk::ImageMemoryBarrier2 toTransfer2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = (format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint) ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + vk::DependencyInfo depToTransfer{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &toTransfer2}; + cb.pipelineBarrier2(depToTransfer); + // Copy + cb.copyBufferToImage(staging, image, vk::ImageLayout::eTransferDstOptimal, regions); + // After copy, if we'll generate mips, keep level 0 in TRANSFER_SRC and leave others in TRANSFER_DST. + // Else transition ALL levels to SHADER_READ_ONLY. (Sync2) + const bool willGenerateMips = (mipLevels > 1 && regions.size() == 1); + if (willGenerateMips) { + vk::ImageMemoryBarrier2 postCopy2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eNone, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eTransferSrcOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = (format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint) ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + vk::DependencyInfo depPostCopy{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &postCopy2}; + cb.pipelineBarrier2(depPostCopy); + } else { + vk::ImageMemoryBarrier2 allToSample{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eNone, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = (format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint) ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + vk::DependencyInfo depAllToSample{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &allToSample}; + cb.pipelineBarrier2(depAllToSample); + } + cb.end(); + + // Submit once on the TRANSFER queue; signal uploads timeline if available + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = !!*uploadsTimeline; + uint64_t signalValue = 0; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + if (canSignalTimeline) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + + transferQueue.submit(submit, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); + + // Perf accounting + auto t1 = std::chrono::steady_clock::now(); + auto ns = std::chrono::duration_cast(t1 - t0).count(); + totalUploadNs.fetch_add(static_cast(ns), std::memory_order_relaxed); + uploadCount.fetch_add(1, std::memory_order_relaxed); + if (stagedBytes > 0) { + bytesUploadedTotal.fetch_add(static_cast(stagedBytes), std::memory_order_relaxed); + } + } catch (const std::exception& e) { + std::cerr << "uploadImageFromStaging failed: " << e.what() << std::endl; + throw; + } +} + +// Generate full mip chain with linear blits (RGBA formats). Assumes level 0 is in TRANSFER_SRC_OPTIMAL. +void Renderer::generateMipmaps(vk::Image image, + vk::Format format, + int32_t texWidth, + int32_t texHeight, + uint32_t mipLevels) { + ensureThreadLocalVulkanInit(); + // Verify format supports linear blit + auto props = physicalDevice.getFormatProperties(format); + if ((props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear) == vk::FormatFeatureFlags{}) { + return; // no linear filter support; skip + } + + vk::CommandPoolCreateInfo poolInfo{.flags = vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueFamilyIndices.graphicsFamily.value()}; + vk::raii::CommandPool tempPool(device, poolInfo); + vk::CommandBufferAllocateInfo allocInfo{.commandPool = *tempPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; + vk::raii::CommandBuffers cbs(device, allocInfo); + vk::raii::CommandBuffer& cb = cbs[0]; + cb.begin({.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + int32_t mipW = texWidth; + int32_t mipH = texHeight; + for (uint32_t i = 1; i < mipLevels; ++i) { + // Transition level i to TRANSFER_DST (Sync2) + vk::ImageMemoryBarrier2 toDst2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, .srcAccessMask = vk::AccessFlagBits2::eNone, .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eTransferDstOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = image, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, i, 1, 0, 1} + }; + vk::DependencyInfo depToDst{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &toDst2}; + cb.pipelineBarrier2(depToDst); + + // Blit from i-1 to i + vk::ImageBlit blit{}; + blit.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; + blit.srcSubresource.mipLevel = i - 1; + blit.srcSubresource.baseArrayLayer = 0; + blit.srcSubresource.layerCount = 1; + blit.srcOffsets[0] = vk::Offset3D{0, 0, 0}; + blit.srcOffsets[1] = vk::Offset3D{mipW, mipH, 1}; + blit.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; + blit.dstSubresource.mipLevel = i; + blit.dstSubresource.baseArrayLayer = 0; + blit.dstSubresource.layerCount = 1; + blit.dstOffsets[0] = vk::Offset3D{0, 0, 0}; + blit.dstOffsets[1] = vk::Offset3D{std::max(1, mipW / 2), std::max(1, mipH / 2), 1}; + cb.blitImage(image, vk::ImageLayout::eTransferSrcOptimal, image, vk::ImageLayout::eTransferDstOptimal, blit, vk::Filter::eLinear); + + // Transition previous level to SHADER_READ_ONLY (Sync2) + vk::ImageMemoryBarrier2 prevToRead2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, .srcAccessMask = vk::AccessFlagBits2::eTransferRead, .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, .dstAccessMask = vk::AccessFlagBits2::eShaderRead, .oldLayout = vk::ImageLayout::eTransferSrcOptimal, .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = image, .subresourceRange = {vk::ImageAspectFlagBits::eColor, i - 1, 1, 0, 1} + }; + vk::DependencyInfo depPrevToRead{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &prevToRead2}; + cb.pipelineBarrier2(depPrevToRead); + + mipW = std::max(1, mipW / 2); + mipH = std::max(1, mipH / 2); + } + // Transition last level to SHADER_READ_ONLY (Sync2) + vk::ImageMemoryBarrier2 lastToRead2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, .dstAccessMask = vk::AccessFlagBits2::eShaderRead, .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = image, .subresourceRange = {vk::ImageAspectFlagBits::eColor, mipLevels - 1, 1, 0, 1} + }; + vk::DependencyInfo depLastToRead{.dependencyFlags = vk::DependencyFlagBits::eByRegion, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &lastToRead2}; + cb.pipelineBarrier2(depLastToRead); + + cb.end(); + + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = !!*uploadsTimeline; + uint64_t signalValue = 0; { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit + if (canSignalTimeline) { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + graphicsQueue.submit(submit, *fence); + } + (void) waitForFencesSafe(*fence, VK_TRUE); +} diff --git a/attachments/openxr_engine/shaders/common_types.slang b/attachments/openxr_engine/shaders/common_types.slang new file mode 100644 index 000000000..c0638a04a --- /dev/null +++ b/attachments/openxr_engine/shaders/common_types.slang @@ -0,0 +1,205 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Common types and structures shared between rasterization and ray query shaders +// This module contains data structures that match the CPU-side definitions + +// Light data structure for storage buffer +struct LightData { + [[vk::offset(0)]] float4 position; + [[vk::offset(16)]] float4 color; + [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; + [[vk::offset(96)]] float4 direction; + [[vk::offset(112)]] int lightType; + [[vk::offset(116)]] float range; + [[vk::offset(120)]] float innerConeAngle; + [[vk::offset(124)]] float outerConeAngle; +}; + +// Uniform buffer object +struct UniformBufferObject { + float4x4 model; + float4x4 viewProjections[4]; // Matches Chapter 8 tutorial + float4x4 views[4]; + float4x4 projs[4]; + float4 camPoses[4]; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; + int lightCount; + int padding0; + float padding1; + float padding2; + float2 screenDimensions; + float nearZ; + float farZ; + float slicesZ; + float _pad3; + // Planar reflections + float4x4 reflectionVP; // projection * mirroredView + int reflectionEnabled; // 1 when sampling reflection in main pass + int reflectionPass; // 1 during reflection render pass + float2 _reflectPad0; + float4 clipPlaneWS; // world-space plane ax+by+cz+d=0 + // Controls + float reflectionIntensity; // scales reflection mix in glass + int enableRayQueryReflections; // 1 to enable reflections in ray query mode + int enableRayQueryTransparency; // 1 to enable transparency/refraction in ray query mode + float _padReflect[1]; + // Ray-query specific: number of entries in geometryInfoBuffer (per-instance) + int geometryInfoCount; + // Keep CPU/GPU layout identical to C++ (renderer.h) + int _padGeo0; + int _padGeo1; + int _padGeo2; + float4 _rqReservedWorldPos; + // Ray-query specific: number of materials in materialBuffer (for bounds) + int materialCount; + int _padMat0; + int _padMat1; + int _padMat2; +}; + +// Push constants for material properties +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; + float3 emissiveFactor; + float emissiveStrength; + float transmissionFactor; + int useSpecGlossWorkflow; + float glossinessFactor; + float3 specularFactor; + float ior; + bool hasEmissiveStrengthExt; +}; + +// Forward+ per-tile header +struct TileHeader { + uint offset; + uint count; + uint pad0; + uint pad1; +}; + +// Ray-query shared buffers (also reused by raster PBR when doing ray-query shadows) +// Layout must match the CPU-side structs in `renderer.h` and the definitions in `ray_query.slang`. +struct GeometryInfo { + uint64_t vertexBufferAddress; + uint64_t indexBufferAddress; + uint vertexCount; + uint materialIndex; + uint indexCount; // number of indices in the index buffer + uint _pad0; + // Instance -> world normal transform (3 columns; xyz used, w unused) + float4 normalMatrix0; + float4 normalMatrix1; + float4 normalMatrix2; +}; + +struct MaterialData { + float3 albedo; + float metallic; + float3 emissive; + float roughness; + float ao; + float ior; + float emissiveStrength; + float alpha; + float transmissionFactor; + float alphaCutoff; + int alphaMode; // 0=OPAQUE, 1=MASK, 2=BLEND (matches glTF) + uint isGlass; + uint isLiquid; + + // Thick-glass parameters (RQ-only) + float3 absorptionColor; // Color after traveling absorptionDistance in the medium (1=none) + float absorptionDistance; // Distance at which absorptionColor applies (meters) + uint thinWalled; // 1 = thin surface (no thickness), 0 = thick volume + + // Raster parity: texture-set flags (-1 = no texture; 0 = sample from texture table) + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + + // Ray Query texture table indices (binding 6) + int baseColorTexIndex; + int normalTexIndex; + int physicalTexIndex; + int occlusionTexIndex; + int emissiveTexIndex; + + // Specular-glossiness workflow support + int useSpecGlossWorkflow; + float glossinessFactor; + float3 specularFactor; + int hasEmissiveStrengthExt; + uint _padMat[3]; +}; + +// Constants +static const float PI = 3.14159265359; + +// Matrix inverse utility (4x4 only) +float4x4 inverse(float4x4 m) { + float n11 = m[0][0], n12 = m[1][0], n13 = m[2][0], n14 = m[3][0]; + float n21 = m[0][1], n22 = m[1][1], n23 = m[2][1], n24 = m[3][1]; + float n31 = m[0][2], n32 = m[1][2], n33 = m[2][2], n34 = m[3][2]; + float n41 = m[0][3], n42 = m[1][3], n43 = m[2][3], n44 = m[3][3]; + + float t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44; + float t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44; + float t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44; + float t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34; + + float det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14; + float idet = 1.0 / det; + + float4x4 ret; + ret[0][0] = t11 * idet; + ret[0][1] = (n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44) * idet; + ret[0][2] = (n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44) * idet; + ret[0][3] = (n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43) * idet; + + ret[1][0] = t12 * idet; + ret[1][1] = (n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44) * idet; + ret[1][2] = (n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44) * idet; + ret[1][3] = (n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43) * idet; + + ret[2][0] = t13 * idet; + ret[2][1] = (n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44) * idet; + ret[2][2] = (n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44) * idet; + ret[2][3] = (n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43) * idet; + + ret[3][0] = t14 * idet; + ret[3][1] = (n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34) * idet; + ret[3][2] = (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) * idet; + ret[3][3] = (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) * idet; + + return ret; +} diff --git a/attachments/openxr_engine/shaders/composite.slang b/attachments/openxr_engine/shaders/composite.slang new file mode 100644 index 000000000..01f321332 --- /dev/null +++ b/attachments/openxr_engine/shaders/composite.slang @@ -0,0 +1,74 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Fullscreen composite pass: samples the off-screen opaque color and writes to swapchain + +import tonemapping_utils; + +struct VSOut { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; +}; + +// Export entrypoint for vertex stage +[shader("vertex")] VSOut VSMain(uint vid : SV_VertexID) +{ + // Fullscreen triangle (no vertex buffer) + float2 pos = float2( (vid == 2) ? 3.0 : -1.0, + (vid == 1) ? 3.0 : -1.0 ); + float2 uv = float2( (vid == 2) ? 2.0 : 0.0, + (vid == 1) ? 2.0 : 0.0 ); + VSOut o; + o.Position = float4(pos, 0.0, 1.0); + o.UV = uv; + return o; +} + +// Set 0, binding 0: combined image sampler for the off-screen scene color +[[vk::binding(0, 0)]] Sampler2D sceneColor; + +struct Push { + float exposure; + float gamma; + int outputIsSRGB; // 1 when the color attachment is SRGB; 0 otherwise + float _pad; // pad to 16 bytes for push constant layout +}; +[[vk::push_constant]] Push pushConsts; + +// Export entrypoint for fragment stage +[shader("fragment")] float4 PSMain(VSOut i) : SV_TARGET +{ + float4 c = sceneColor.Sample(i.UV); + float3 color = c.rgb; + + // Apply exposure and filmic tonemapping + color *= pushConsts.exposure; + + // Uncharted2 / Hable filmic tonemap, canonical form + float3 t = Hable_Filmic_Tonemapping::Uncharted2Tonemap(color); + float3 w = Hable_Filmic_Tonemapping::Uncharted2Tonemap(float3(1,1,1) * Hable_Filmic_Tonemapping::W); + color = t / max(w, float3(1e-6, 1e-6, 1e-6)); + + // If the attachment is NOT SRGB, encode gamma here. When it is SRGB, + // the hardware will encode at store so we keep color in linear space. + if (pushConsts.outputIsSRGB == 0) { + color = pow(max(color, 0.0), float3(1.0 / pushConsts.gamma)); + } else { + color = saturate(color); + } + return float4(color, 1.0); +} diff --git a/attachments/openxr_engine/shaders/forward_plus_cull.slang b/attachments/openxr_engine/shaders/forward_plus_cull.slang new file mode 100644 index 000000000..86eb1e5e6 --- /dev/null +++ b/attachments/openxr_engine/shaders/forward_plus_cull.slang @@ -0,0 +1,136 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Forward+ tiled light culling (2D tiles) + +// Use the shared `LightData`/`TileHeader` definitions +import common_types; + +// Params packed by the engine (see updateForwardPlusParams) +struct FPParams { + column_major float4x4 view; + column_major float4x4 proj; + float4 screenTile; // x=width,y=height,z=tileX,w=tileY + uint4 counts; // x=lightCount,y=maxPerTile,z=tilesX,w=tilesY + float4 zParams; // x=nearZ, y=farZ, z=slicesZ, w=0 +}; + +[[vk::binding(0, 0)]] StructuredBuffer lightsRO; +[[vk::binding(1, 0)]] RWStructuredBuffer tileHeadersRW; +[[vk::binding(2, 0)]] RWStructuredBuffer tileLightIndicesRW; +[[vk::binding(3, 0)]] ConstantBuffer params; + +// NOTE: This implementation performs a conservative 2D geometric test per tile: +// it projects each point light to screen-space and computes an approximate screen-space +// radius from its world-space range using the projection matrix. A light is included in +// a tile if the circle intersects the tile rectangle. Depth/cluster slicing can be added later. + +[numthreads(1, 1, 1)] +void main(uint3 DTid : SV_DispatchThreadID) +{ + uint tilesX = params.counts.z; + uint tilesY = params.counts.w; + uint slicesZ = (uint)params.zParams.z; + uint maxPerTile = params.counts.y; + uint lightCount = params.counts.x; + + uint cx = min(DTid.x, (tilesX > 0) ? tilesX - 1 : 0); + uint cy = min(DTid.y, (tilesY > 0) ? tilesY - 1 : 0); + uint cz = (slicesZ > 0) ? min(DTid.z, slicesZ - 1) : 0; + + uint tileId = (cz * tilesY + cy) * tilesX + cx; + + // Screen and tile metrics + float2 screenSize = params.screenTile.xy; + float2 tileSize = params.screenTile.zw; // (tileXSize, tileYSize) + float2 tileMin = float2(cx, cy) * tileSize; + float2 tileMax = tileMin + tileSize; + + uint base = tileId * maxPerTile; + uint count = 0; + + // Precompute projection scaling terms to estimate screen-space radius + // For a perspective matrix, proj[0][0] and proj[1][1] scale x/y by f/z. + float projXX = params.proj[0][0]; + float projYY = params.proj[1][1]; + + // Log-sliced depth range for this cluster (positive distances) + float nearZ = max(params.zParams.x, 1e-3); + float farZ = max(params.zParams.y, nearZ + 1e-3); + float fcz0 = (slicesZ > 0) ? (float(cz) / float(slicesZ)) : 0.0; + float fcz1 = (slicesZ > 0) ? (float(cz + 1) / float(slicesZ)) : 1.0; + float sliceNear = exp(lerp(log(nearZ), log(farZ), fcz0)); + float sliceFar = exp(lerp(log(nearZ), log(farZ), fcz1)); + + // Iterate over all lights and append those intersecting this tile + [loop] + for (uint li = 0; li < lightCount; ++li) + { + if (count >= maxPerTile) { break; } + + LightData L = lightsRO[li]; + + // Only point and spot lights have finite range spheres; treat directional as global (include all tiles/slices) + bool isDirectional = (L.lightType == 1); + bool includeAll = isDirectional; + + float2 centerPx = float2(0.0, 0.0); + float radiusPx = 1e9; // huge for directional + bool zOverlap = true; + + if (!includeAll) + { + // Transform light center to view space + float4 posVS = mul(params.view, float4(L.position.xyz, 1.0)); + + // Use positive depth distance + float z = max(1e-3, abs(posVS.z)); + + // Z overlap test with this slice + float zMin = max(0.0, z - L.range); + float zMax = z + L.range; + zOverlap = (zMax >= sliceNear) && (zMin <= sliceFar); + + // Project to clip then NDC + float4 clip = mul(params.proj, float4(posVS.xyz, 1.0)); + float invW = (clip.w != 0.0) ? rcp(clip.w) : 0.0; + float2 ndc = clip.xy * invW; // [-1,1] + centerPx = (ndc * 0.5 + 0.5) * screenSize; // pixels + + // Approximate screen-space radius from world radius (range) + float rx = abs(L.range * projXX / z) * (screenSize.x * 0.5); + float ry = abs(L.range * projYY / z) * (screenSize.y * 0.5); + radiusPx = max(rx, ry); + } + + // Circle vs axis-aligned rectangle overlap test (conservative) + float2 closest = clamp(centerPx, tileMin, tileMax); + float2 d = closest - centerPx; + float dist2 = dot(d, d); + if (zOverlap && dist2 <= radiusPx * radiusPx) + { + tileLightIndicesRW[base + count] = li; + count++; + } + } + + // Write header + TileHeader hdr; + hdr.offset = base; + hdr.count = count; + hdr.pad0 = 0; hdr.pad1 = 0; + tileHeadersRW[tileId] = hdr; +} diff --git a/attachments/openxr_engine/shaders/hrtf.slang b/attachments/openxr_engine/shaders/hrtf.slang new file mode 100644 index 000000000..3ebd59fce --- /dev/null +++ b/attachments/openxr_engine/shaders/hrtf.slang @@ -0,0 +1,262 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Compute shader for HRTF (Head-Related Transfer Function) audio processing +// This shader processes audio data to create 3D spatial audio effects + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer inputAudioBuffer; // Raw audio samples +[[vk::binding(1, 0)]] RWStructuredBuffer outputAudioBuffer; // Processed audio samples +[[vk::binding(2, 0)]] StructuredBuffer hrtfData; // HRTF impulse responses +[[vk::binding(3, 0)]] ConstantBuffer params; // HRTF parameters + +// Parameters for HRTF processing - MUST match CPU GPUHRTFParams structure exactly +struct HRTFParams { + float4 listenerPosition; // Position of the listener (float[4] on CPU) - 16 bytes + float4 listenerForward; // Forward direction of the listener (float[4] on CPU) - 16 bytes + float4 listenerUp; // Up direction of the listener (float[4] on CPU) - 16 bytes + float4 sourcePosition; // Position of the sound source (float[4] on CPU) - 16 bytes + float sampleCount; // Number of samples to process (4 bytes) - offset 64 + float3 padding1; // Padding to align to 16-byte boundary (12 bytes) - offset 68 + uint inputChannels; // Number of input channels (4 bytes) - offset 80 + uint outputChannels; // Number of output channels (4 bytes) - offset 84 + uint hrtfSize; // Size of each HRTF impulse response (4 bytes) - offset 88 + uint numHrtfPositions; // Number of HRTF positions (4 bytes) - offset 92 + float distanceAttenuation; // Distance attenuation factor (4 bytes) - offset 96 + float dopplerFactor; // Doppler effect factor (4 bytes) - offset 100 + float reverbMix; // Reverb mix factor (4 bytes) - offset 104 + float padding2; // Padding to complete 16-byte alignment (4 bytes) - offset 108 +}; + +// Helper function to calculate the index of the closest HRTF in the dataset +uint FindClosestHRTF(float azimuth, float elevation) { + // This is a simplified implementation + // In a real implementation, this would find the closest HRTF in the dataset + // based on the azimuth and elevation angles + + // Normalize azimuth to [0, 360) degrees + azimuth = fmod(azimuth + 360.0, 360.0); + + // Clamp elevation to [-90, 90] degrees + elevation = clamp(elevation, -90.0, 90.0); + + // Calculate indices based on a typical HRTF dataset layout + // Assuming 10-degree resolution in azimuth and 15-degree in elevation + uint azimuthIndex = uint(round(azimuth / 10.0)) % 36; + uint elevationIndex = uint(round((elevation + 90.0) / 15.0)) % 13; + + // Calculate the final index + return elevationIndex * 36 + azimuthIndex; +} + +// Helper function to calculate azimuth and elevation angles +void CalculateAngles(float3 sourceDir, float3 listenerForward, float3 listenerUp, out float azimuth, out float elevation) { + // Simplified angle calculation - directly use source direction + // Calculate azimuth (horizontal angle) - angle around Y axis + azimuth = atan2(sourceDir.x, -sourceDir.z) * 57.2957795; // Convert to degrees, negate z for correct orientation + + // Calculate elevation (vertical angle) - angle from horizontal plane + float horizontalLength = sqrt(sourceDir.x * sourceDir.x + sourceDir.z * sourceDir.z); + elevation = atan2(sourceDir.y, horizontalLength) * 57.2957795; // Convert to degrees +} + +// Main compute shader function +[shader("compute")] +[numthreads(64, 1, 1)] +void main(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if the thread is within bounds + if (index >= uint(params.sampleCount)) { + return; + } + + // STAGE 1: HRTF DATA ACCESS WITH SAFETY VALIDATION + // Start with working basic panning and add HRTF data access + + // Get input sample for this thread + float inputSample = inputAudioBuffer[index]; + + // STAGE 1: Test HRTF data buffer access with ultra-safe bounds checking + bool hrtfDataValid = false; + float testHrtfSample = 0.0f; + + // Ultra-safe HRTF data access test + if (params.hrtfSize > 0 && params.numHrtfPositions > 0) { + // Test access to first HRTF sample with multiple safety checks + uint testHrtfIndex = 0; // Start with first sample + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; // 2 channels + + if (testHrtfIndex < maxHrtfBufferSize && testHrtfIndex < 500000) { // Additional hardcoded safety limit + testHrtfSample = hrtfData[testHrtfIndex]; + hrtfDataValid = true; + } + } + + // STAGE 2: 3D DIRECTION CALCULATION AND ANGLE COMPUTATION + // Calculate 3D direction from listener to source + float3 sourceDir = params.sourcePosition.xyz - params.listenerPosition.xyz; + float distance = length(sourceDir); + + // Handle edge case where listener and source are at same position + if (distance < 0.001) { + sourceDir = float3(0.0, 0.0, -1.0); // Default to front direction + distance = 1.0; + } else { + sourceDir = normalize(sourceDir); + } + + // Calculate azimuth and elevation angles using the helper function + float azimuth, elevation; + CalculateAngles(sourceDir, params.listenerForward.xyz, params.listenerUp.xyz, azimuth, elevation); + + + // ENHANCED SPATIAL PROCESSING: Use 3D angles for better panning + float leftGain = 1.0; + float rightGain = 1.0; + + // Convert azimuth to left/right panning (-180 to +180 degrees) + // Positive azimuth = right side, negative = left side + if (azimuth > 0.0) { + // Source is to the right, reduce left channel based on angle + float rightness = min(1.0, azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = max(0.2, 1.0 - rightness * 0.8); // Reduce left by up to 80% + rightGain = 1.0; + } else if (azimuth < 0.0) { + // Source is to the left, reduce right channel based on angle + float leftness = min(1.0, -azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = 1.0; + rightGain = max(0.2, 1.0 - leftness * 0.8); // Reduce right by up to 80% + } + + // Apply distance attenuation (closer sources are louder) + float distanceAttenuation = 1.0 / max(1.0, distance * 0.5); // Gentle distance falloff + leftGain *= distanceAttenuation; + rightGain *= distanceAttenuation; + + // STAGE 3: HRTF INDEX LOOKUP WITH BOUNDS CHECKING + // Find the closest HRTF in the dataset based on calculated angles + uint hrtfIndex = FindClosestHRTF(azimuth, elevation); + + // Ultra-safe bounds checking for HRTF index + bool hrtfIndexValid = false; + if (hrtfIndex < params.numHrtfPositions && params.numHrtfPositions > 0) { + hrtfIndexValid = true; + } + + // ENHANCED HRTF DATA ACCESS: Use calculated index instead of just first sample + float hrtfLeftSample = 0.0f; + float hrtfRightSample = 0.0f; + bool hrtfSamplesValid = false; + + if (hrtfIndexValid && hrtfDataValid) { + // Calculate HRTF buffer offsets for left and right channels + // HRTF data layout: [position0_left_samples][position0_right_samples][position1_left_samples]... + uint leftChannelOffset = hrtfIndex * params.hrtfSize * 2; // 2 channels per position + uint rightChannelOffset = leftChannelOffset + params.hrtfSize; + + // Ultra-safe bounds checking for HRTF sample access + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + if (leftChannelOffset < maxHrtfBufferSize && rightChannelOffset < maxHrtfBufferSize && + leftChannelOffset < 500000 && rightChannelOffset < 500000) { // Additional hardcoded safety + + // Access first sample of each channel's impulse response for this position + hrtfLeftSample = hrtfData[leftChannelOffset]; + hrtfRightSample = hrtfData[rightChannelOffset]; + hrtfSamplesValid = true; + } + } + + // STAGE 4: HRTF CONVOLUTION LOOP WITH ULTRA-SAFE MEMORY ACCESS + float leftConvolution = 0.0f; + float rightConvolution = 0.0f; + uint convolutionSamples = 0; + + if (hrtfIndexValid && hrtfDataValid && params.hrtfSize > 0) { + // Calculate base offsets for this HRTF position + uint leftChannelBase = hrtfIndex * params.hrtfSize * 2; + uint rightChannelBase = leftChannelBase + params.hrtfSize; + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + + // Limit convolution size for safety and performance + uint safeHrtfSize = min(params.hrtfSize, 32u); // Limit to 32 samples for safety + + // HRTF Convolution loop with ultra-safe bounds checking + for (uint i = 0; i < safeHrtfSize; i++) { + // Check if we can access the input audio sample + if (index >= i) { + uint inputIndex = index - i; + + // Ultra-safe input buffer bounds check + if (inputIndex < uint(params.sampleCount) && inputIndex < 1024) { + float audioSample = inputAudioBuffer[inputIndex]; + + // Calculate HRTF sample indices with bounds checking + uint leftHrtfIndex = leftChannelBase + i; + uint rightHrtfIndex = rightChannelBase + i; + + // Ultra-safe HRTF buffer bounds check + if (leftHrtfIndex < maxHrtfBufferSize && rightHrtfIndex < maxHrtfBufferSize && + leftHrtfIndex < 500000 && rightHrtfIndex < 500000) { + + float leftHrtfSample = hrtfData[leftHrtfIndex]; + float rightHrtfSample = hrtfData[rightHrtfIndex]; + + // Apply convolution + leftConvolution += audioSample * leftHrtfSample; + rightConvolution += audioSample * rightHrtfSample; + convolutionSamples++; + } + } + } + } + + } + + // STAGE 4: Apply convolution results with distance attenuation + if (convolutionSamples > 0) { + // Use convolution results instead of simple gain modification + leftGain = leftConvolution * distanceAttenuation; + rightGain = rightConvolution * distanceAttenuation; + } + + + // STAGE 5: COMPLETE HRTF PROCESSING - FINAL OUTPUT WITH OPTIMIZATION + // Write to both output channels with full HRTF processing + for (uint channel = 0; channel < 2; channel++) { // Hardcode to 2 channels for safety + uint outputIndex = index * 2 + channel; + + // Ultra-safe bounds check with hardcoded limits + if (outputIndex < 1024 * 2 && outputIndex < 2048) { + float finalSample = 0.0f; + + if (convolutionSamples > 0) { + // STAGE 5: Use full HRTF convolution results + finalSample = (channel == 0) ? leftGain : rightGain; + + // Apply output normalization to prevent clipping + finalSample = clamp(finalSample, -1.0f, 1.0f); + } else { + // Fallback: Enhanced spatial panning + float channelGain = (channel == 0) ? leftGain : rightGain; + finalSample = inputSample * channelGain; + } + + outputAudioBuffer[outputIndex] = finalSample; + } + } + +} diff --git a/attachments/openxr_engine/shaders/imgui.slang b/attachments/openxr_engine/shaders/imgui.slang new file mode 100644 index 000000000..3d10f7cc1 --- /dev/null +++ b/attachments/openxr_engine/shaders/imgui.slang @@ -0,0 +1,66 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Combined vertex and fragment shader for ImGui rendering + +// Input from vertex buffer +struct VSInput { + float2 Position : POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Push constants for transformation +struct PushConstants { + float2 Scale; + float2 Translate; +}; + +// Bindings +[[vk::push_constant]] PushConstants pushConstants; +[[vk::binding(0, 0)]] Sampler2D fontTexture; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position + output.Position = float4(input.Position * pushConstants.Scale + pushConstants.Translate, 0.0, 1.0); + + // Pass UV and color to fragment shader + output.UV = input.UV; + output.Color = input.Color; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample font texture and multiply by color + float4 color = input.Color * fontTexture.Sample(input.UV); + return color; +} diff --git a/attachments/openxr_engine/shaders/lighting.slang b/attachments/openxr_engine/shaders/lighting.slang new file mode 100644 index 000000000..5d673cc8d --- /dev/null +++ b/attachments/openxr_engine/shaders/lighting.slang @@ -0,0 +1,116 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Combined vertex and fragment shader for basic/legacy lighting +// This shader implements the Phong lighting model as a fallback when BRDF/PBR is disabled +// Note: BRDF/PBR is now the default lighting model - this is used only when explicitly requested + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Added to match vertex layout (unused in basic lighting) +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent (unused in basic lighting) +}; + +// Uniform buffer for transformation matrices and light information +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPos; + float4 lightColor; + float4 viewPos; +}; + +// Push constants for material properties +struct PushConstants { + float4 ambientColor; + float4 diffuseColor; + float4 specularColor; + float shininess; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D texSampler; + +// Push constants +[[vk::push_constant]] PushConstants material; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.TexCoord = input.TexCoord; + + // Pass tangent (unused in basic lighting but required for vertex layout compatibility) + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample texture + float4 texColor = texSampler.Sample(input.TexCoord); + + // Normalize vectors + float3 normal = normalize(input.Normal); + float3 lightDir = normalize(ubo.lightPos.xyz - input.WorldPos); + float3 viewDir = normalize(ubo.viewPos.xyz - input.WorldPos); + float3 reflectDir = reflect(-lightDir, normal); + + // Ambient + float3 ambient = material.ambientColor.rgb * ubo.lightColor.rgb; + + // Diffuse + float diff = max(dot(normal, lightDir), 0.0); + float3 diffuse = diff * material.diffuseColor.rgb * ubo.lightColor.rgb; + + // Specular + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float3 specular = spec * material.specularColor.rgb * ubo.lightColor.rgb; + + // Combine components + float3 result = (ambient + diffuse + specular) * texColor.rgb; + + return float4(result, texColor.a); +} diff --git a/attachments/openxr_engine/shaders/lighting_utils.slang b/attachments/openxr_engine/shaders/lighting_utils.slang new file mode 100644 index 000000000..1ecb3fae7 --- /dev/null +++ b/attachments/openxr_engine/shaders/lighting_utils.slang @@ -0,0 +1,119 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Lighting utilities for evaluating lights and accumulating BRDF contributions +// Shared between rasterization and ray query shaders + +import common_types; +import pbr_utils; + +// Result of evaluating a single light +struct LightEvaluation { + float3 L; // Direction to light (normalized) + float3 radiance; // Incident radiance + float NdotL; // Clamped N·L for BRDF evaluation + bool valid; // True if light contributes +}; + +// Evaluate a single light at a world position +// Returns light direction, radiance, and N·L +LightEvaluation evaluateLight(LightData light, float3 worldPos, float3 N) { + LightEvaluation result; + result.valid = false; + + if (light.lightType == 1) { + // Directional light + result.L = normalize(-light.position.xyz); + result.radiance = light.color.rgb; + result.valid = true; + } else { + // Point/spot/emissive light: position.xyz is light position in world space + float3 toLight = light.position.xyz - worldPos; + float d = length(toLight); + result.L = (d > 1e-5) ? toLight / d : float3(0, 0, 1); + + if (light.lightType == 3) { + // Emissive light: soft falloff using range as characteristic radius + float r = max(light.range, 0.001); + float att = 1.0 / (1.0 + (d / r) * (d / r)); + result.radiance = light.color.rgb * att; + result.valid = true; + } else if (light.lightType == 0 || light.lightType == 2) { + // Point or spot light: inverse square falloff with range windowing + float attenuation = 1.0 / max(d * d, 0.0001); + + // GLTF style range attenuation + if (light.range > 0.0) { + attenuation *= pow(saturate(1.0 - pow(d / light.range, 4.0)), 2.0); + } + + result.radiance = light.color.rgb * attenuation; + + if (light.lightType == 2) { + // Spot light cone attenuation + float3 D = normalize(light.direction.xyz); + float cd = dot(D, -result.L); + float cosInner = cos(light.innerConeAngle); + float cosOuter = cos(light.outerConeAngle); + float spotAttenuation = saturate((cd - cosOuter) / max(cosInner - cosOuter, 0.0001)); + spotAttenuation *= spotAttenuation; + result.radiance *= spotAttenuation; + } + result.valid = true; + } + } + + if (result.valid) { + // For emissive lights, treat lighting as two-sided to avoid self-occlusion + float rawDot = dot(N, result.L); + result.NdotL = (light.lightType == 3) ? abs(rawDot) : max(rawDot, 0.0); + result.valid = (result.NdotL > 0.0); + } + + return result; +} + +// Accumulate lighting contribution from a single light using GGX BRDF +// Adds diffuse and specular contributions to the provided accumulators +void accumulateLighting( + LightEvaluation lightEval, + float3 N, + float3 V, + float3 albedo, + float metallic, + float roughness, + float3 F0, + inout float3 diffuseLighting, + inout float3 specularLighting) +{ + if (!lightEval.valid) return; + + float3 H = normalize(V + lightEval.L); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // GGX microfacet BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, lightEval.NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 spec = (D * G * F) / max(4.0 * NdotV * lightEval.NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metallic); + + specularLighting += spec * lightEval.radiance * lightEval.NdotL; + diffuseLighting += (kD * albedo / PI) * lightEval.radiance * lightEval.NdotL; +} diff --git a/attachments/openxr_engine/shaders/pbr.slang b/attachments/openxr_engine/shaders/pbr.slang new file mode 100644 index 000000000..83c705e2e --- /dev/null +++ b/attachments/openxr_engine/shaders/pbr.slang @@ -0,0 +1,625 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Import shared utility modules +import common_types; +import pbr_utils; +import lighting_utils; +import tonemapping_utils; + +// Input from vertex buffer +struct VSInput { + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 UV; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data. Model matrix is a true 4x4, while the normal + // matrix is provided as three float4 columns that match the CPU + // layout (glm::mat3x4: 3 columns of vec4, xyz used, w unused). + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (uses 4 locations) + [[vk::location(8)]] float4 InstanceNormal0; // normal matrix column 0 + [[vk::location(9)]] float4 InstanceNormal1; // normal matrix column 1 + [[vk::location(10)]] float4 InstanceNormal2; // normal matrix column 2 +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos; + float3 Normal : NORMAL; + float3 GeometricNormal : NORMAL1; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; + uint ViewID : SV_ViewID; +}; + +[[vk::binding(0, 1)]] Sampler2D opaqueSceneColor; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D baseColorMap; +[[vk::binding(2, 0)]] Sampler2D metallicRoughnessMap; +[[vk::binding(3, 0)]] Sampler2D normalMap; +[[vk::binding(4, 0)]] Sampler2D occlusionMap; +[[vk::binding(5, 0)]] Sampler2D emissiveMap; +[[vk::binding(6, 0)]] StructuredBuffer lightBuffer; +// Forward+ per-tile light lists (same set 0 to keep pipeline layouts compact) +[[vk::binding(7, 0)]] StructuredBuffer tileHeaders; +[[vk::binding(8, 0)]] StructuredBuffer tileLightIndices; +// Planar reflection sampler (bound only when reflections are enabled) +[[vk::binding(10, 0)]] Sampler2D reflectionMap; + +// Raster ray-query shadows: TLAS +[[vk::binding(11, 0)]] RaytracingAccelerationStructure tlas; + +// Ray-query shared buffers (used for material-aware raster shadow queries) +[[vk::binding(12, 0)]] StructuredBuffer geometryInfoBuffer; +[[vk::binding(13, 0)]] StructuredBuffer materialBuffer; + +[[vk::push_constant]] PushConstants material; + +static const float RASTER_SHADOW_EPS = 0.001; + +// Hard shadow query for raster fragment shading. +// NOTE: We intentionally treat NON_OPAQUE candidates as non-occluding here. +// To make glass/transmissive surfaces not block light, those instances should +// be flagged as FORCE_NO_OPAQUE in the TLAS build. +bool traceShadowOccluded(float3 origin, float3 direction, float tMin, float tMax) +{ + RayDesc ray; + ray.Origin = origin; + ray.Direction = direction; + ray.TMin = tMin; + ray.TMax = tMax; + + RayQuery q; + // Match TLAS instance masking convention from `renderer_ray_query.cpp`: + // 0x01 = regular scene geometry, 0x02 = environment/sky. + // For raster shadows, ignore the environment to avoid global false occlusion. + uint mask = 0x01; + q.TraceRayInline( + tlas, + RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, + mask, + ray + ); + + int iter = 0; + while (q.Proceed() && iter < 64) + { + iter++; + // No special acceptance rules here; accept-first-hit will commit on the first opaque blocker. + } + return (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT); +} + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input, uint viewID : SV_ViewID) +{ + VSOutput output; + output.ViewID = viewID; + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); + output.Position = mul(ubo.viewProjections[viewID], worldPos); + output.WorldPos = worldPos.xyz; + + // Transform normals correctly: first by the per-instance normal matrix, + // then by the entity model 3x3 (avoid double-applying instance transform). + float3x3 instNormal = float3x3(input.InstanceNormal0.xyz, input.InstanceNormal1.xyz, input.InstanceNormal2.xyz); + float3x3 model3x3 = (float3x3)ubo.model; + float3 worldNormal = normalize(mul(model3x3, mul(instNormal, input.Normal))); + output.Normal = worldNormal; + + // Geometric normal (pre-normal-map) uses the same transform path. + output.GeometricNormal = worldNormal; + + // Transform tangent similarly (approximate with same normal transform path). + float3 worldTangent = normalize(mul(model3x3, mul(instNormal, input.Tangent.xyz))); + output.UV = input.UV; + output.Tangent = float4(worldTangent, input.Tangent.w); + return output; +} + +// Fragment shader entry point for generic PBR materials +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // --- 1. Material Properties --- + float2 uv = float2(input.UV.x, 1.0 - input.UV.y); + float4 baseColor = (material.baseColorTextureSet < 0) ? material.baseColorFactor : baseColorMap.Sample(uv) * material.baseColorFactor; + float4 mrOrSpecGloss = (material.physicalDescriptorTextureSet < 0) ? float4(1.0, 1.0, 1.0, 1.0) : metallicRoughnessMap.Sample(uv); + float metallic = 0.0, roughness = 1.0; + float3 F0, albedo; + + if (material.useSpecGlossWorkflow != 0) { + float3 specColorSG = mrOrSpecGloss.rgb * material.specularFactor; + float gloss = clamp(mrOrSpecGloss.a * material.glossinessFactor, 0.0, 1.0); + roughness = clamp(1.0 - gloss, 0.0, 1.0); + F0 = specColorSG; + albedo = baseColor.rgb * (1.0 - max(F0.r, max(F0.g, F0.b))); + } else { + // glTF metallic-roughness texture packs metallic in B, roughness in G (linear space) + float metallicTex = mrOrSpecGloss.b; + float roughnessTex = mrOrSpecGloss.g; + metallic = clamp(metallicTex * material.metallicFactor, 0.0, 1.0); + roughness = clamp(roughnessTex * material.roughnessFactor, 0.0, 1.0); + F0 = lerp(float3(0.04, 0.04, 0.04), baseColor.rgb, metallic); + albedo = baseColor.rgb * (1.0 - metallic); + } + + float ao = (material.occlusionTextureSet < 0) ? 1.0 : occlusionMap.Sample(uv).r; + + // Emissive: default to constant white when no emissive texture so authored emissiveFactor works per glTF spec. + // If a texture is present but factor is zero, assume (1,1,1) to preserve emissive textures by default. + float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) : emissiveMap.Sample(uv).rgb; + float3 emissiveFactor = material.emissiveFactor; + float3 emissive = emissiveTex * emissiveFactor; + if (material.hasEmissiveStrengthExt) + emissive *= material.emissiveStrength; + + if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { discard; } + + // --- 2. Normal Calculation --- + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + float3 tangentNormal = normalMap.Sample(uv).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + // We flip the V coordinate for all textures (uv.y -> 1-uv.y). In + // tangent space, this corresponds to inverting the bitangent. + // glTF's tangent.w encodes the sign of the bitangent relative to + // cross(N,T) in the *unflipped* UV space, so we must negate it here + // to keep the normal map oriented correctly after the V flip. + float handedness = -input.Tangent.w; + float3 B = normalize(cross(N, T)) * handedness; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(TBN, tangentNormal)); + } + + // Geometric (non-normal-mapped) normal for large-scale effects like Fresnel, + // refraction and screen-space reflections. This stays stable across pixels + // on a flat pane and helps avoid flickering when rotating the camera. + float3 G = normalize(input.GeometricNormal); + + // --- 3. Opaque Lighting Calculation --- + float3 V = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + + float3 diffuseLighting = float3(0.0, 0.0, 0.0); + float3 specularLighting = float3(0.0, 0.0, 0.0); + + // Forward+: compute tile id and iterate culled light list + const uint TILE = 16u; // must match engine configuration + uint tilesX = (uint(ubo.screenDimensions.x) + TILE - 1u) / TILE; + uint tilesY = (uint(ubo.screenDimensions.y) + TILE - 1u) / TILE; + + // SV_POSITION in the fragment stage is in window coordinates. Use robust integer index. + uint px = (uint)max(0.0, input.Position.x); + uint py = (uint)max(0.0, input.Position.y); + uint tileX = (tilesX > 0u) ? min(px / TILE, tilesX - 1u) : 0u; + uint tileY = (tilesY > 0u) ? min(py / TILE, tilesY - 1u) : 0u; + uint totalTiles = max(tilesX * tilesY, 1u); + + // Clustered Z slice index from view-space depth (positive distance) + float dVS = abs(mul(ubo.views[input.ViewID], float4(input.WorldPos, 1.0)).z); + float lnN = log(max(ubo.nearZ, 1e-4)); + float lnF = log(max(ubo.farZ, lnN + 1e-4)); + float denom = max(lnF - lnN, 1e-6); + float slices = max(ubo.slicesZ, 1.0); + float lambda = saturate((log(max(dVS, 1e-4)) - lnN) / denom); + uint slice = (uint)clamp(floor(lambda * slices), 0.0, slices - 1.0); + + uint tileId = (slice * tilesY + tileY) * tilesX + tileX; + + // CPU toggles Forward+ via ubo.padding1 (0 = disabled, non-zero = enabled) + bool useForwardPlus = (ubo.padding1 != 0.0); + + uint base = 0u; + uint count = 0u; + if (useForwardPlus && tileId < totalTiles * (uint)slices) { + TileHeader th = tileHeaders[tileId]; + base = th.offset; + count = th.count; + } + + bool forceGlobal = false; + + // Accumulate per-light diffuse and specular terms using GGX microfacet BRDF. + if (useForwardPlus && !forceGlobal && count > 0) { + // Use Forward+ culled list + for (uint li = 0u; li < count; ++li) { + uint lightIndex = tileLightIndices[base + li]; + LightData light = lightBuffer[lightIndex]; + float3 L, radiance; + float distToLight = 10000.0; + if (light.lightType == 1) { + // Directional + L = normalize(-light.position.xyz); + radiance = light.color.rgb; + } else { + // Point/spot/emissive: position.xyz is light position in world space + float3 toLight = light.position.xyz - input.WorldPos; + float d = length(toLight); + L = (d > 1e-5) ? toLight / d : float3(0,0,1); + distToLight = d; + + float attenuation = 1.0; + if (light.lightType == 3) { + // Emissive: soft falloff using range as a characteristic radius + float r = max(light.range, 0.001); + attenuation = 1.0 / (1.0 + (d / r) * (d / r)); + } else { + attenuation = 1.0 / max(d * d, 0.0001); + // GLTF style range attenuation + if (light.range > 0.0) { + attenuation *= pow(saturate(1.0 - pow(d / light.range, 4.0)), 2.0); + } + } + radiance = light.color.rgb * attenuation; + + if (light.lightType == 2) { + // Spot light cone attenuation + float3 D = normalize(light.direction.xyz); + float cd = dot(D, -L); + float cosInner = cos(light.innerConeAngle); + float cosOuter = cos(light.outerConeAngle); + float spotAttenuation = saturate((cd - cosOuter) / max(cosInner - cosOuter, 0.0001)); + spotAttenuation *= spotAttenuation; + radiance *= spotAttenuation; + } + } + // For emissive lights, treat lighting as two-sided to avoid glass/self-occlusion issues + float rawDot = dot(N, L); + float NdotL = (light.lightType == 3) ? abs(rawDot) : max(rawDot, 0.0); + + if (NdotL > 0.0) { + float visibility = 1.0; + // Raster ray-query shadows are expensive. In Bistro, most lights are emissive + // and casting per-light shadows drops FPS drastically. Shadow only the directional + // light (sun) for now. + if (ubo.padding2 != 0.0 && light.lightType == 1) { + float tMaxShadow = (light.lightType == 1) ? 10000.0 : max(distToLight - RASTER_SHADOW_EPS, RASTER_SHADOW_EPS); + float3 shadowOrigin = input.WorldPos + N * RASTER_SHADOW_EPS; + bool occluded = traceShadowOccluded(shadowOrigin, L, RASTER_SHADOW_EPS, tMaxShadow); + visibility = occluded ? 0.0 : 1.0; + } + + float3 H = normalize(V + L); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + float3 spec = (D * G * F) / max(4.0 * NdotV * NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metallic); + specularLighting += spec * radiance * NdotL * visibility; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL * visibility; + } + } + } + // Global light loop (fallback or forced debug) + // Fallback when Forward+ list is empty but lights exist and not in single-tile mode, + // OR always when forceGlobal flag is enabled. + // If Forward+ is disabled, always use global lights. + // If Forward+ is enabled but lists are empty (e.g., before first dispatch), fall back to global. + if (forceGlobal || !useForwardPlus || (count == 0 && ubo.lightCount > 0)) { + // Fallback path when Forward+ is disabled or lists are not populated yet + for (uint li = 0u; li < (uint)ubo.lightCount; ++li) { + LightData light = lightBuffer[li]; + float3 L, radiance; + float distToLight = 10000.0; + if (light.lightType == 1) { + L = normalize(-light.position.xyz); + radiance = light.color.rgb; + } else { + float3 toLight = light.position.xyz - input.WorldPos; + float d = length(toLight); + L = (d > 1e-5) ? toLight / d : float3(0,0,1); + distToLight = d; + + float attenuation = 1.0; + if (light.lightType == 3) { + float r = max(light.range, 0.001); + attenuation = 1.0 / (1.0 + (d / r) * (d / r)); + } else { + attenuation = 1.0 / max(d * d, 0.0001); + // GLTF style range attenuation + if (light.range > 0.0) { + attenuation *= pow(saturate(1.0 - pow(d / light.range, 4.0)), 2.0); + } + } + radiance = light.color.rgb * attenuation; + + if (light.lightType == 2) { + // Spot light cone attenuation + float3 D = normalize(light.direction.xyz); + float cd = dot(D, -L); + float cosInner = cos(light.innerConeAngle); + float cosOuter = cos(light.outerConeAngle); + float spotAttenuation = saturate((cd - cosOuter) / max(cosInner - cosOuter, 0.0001)); + spotAttenuation *= spotAttenuation; + radiance *= spotAttenuation; + } + } + float NdotL = (light.lightType == 3) ? abs(dot(N, L)) : max(dot(N, L), 0.0); + if (NdotL > 0.0) { + float visibility = 1.0; + if (ubo.padding2 != 0.0 && light.lightType == 1) { + float tMaxShadow = (light.lightType == 1) ? 10000.0 : max(distToLight - RASTER_SHADOW_EPS, RASTER_SHADOW_EPS); + float3 shadowOrigin = input.WorldPos + N * RASTER_SHADOW_EPS; + bool occluded = traceShadowOccluded(shadowOrigin, L, RASTER_SHADOW_EPS, tMaxShadow); + visibility = occluded ? 0.0 : 1.0; + } + + float3 H = normalize(V + L); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + float3 spec = (D * G * F) / max(4.0 * NdotV * NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metallic); + specularLighting += spec * radiance * NdotL * visibility; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL * visibility; + } + } + } + + float3 ambient = albedo * ao * (0.1 * ubo.scaleIBLAmbient); + float3 opaqueLit = diffuseLighting + specularLighting + ambient + emissive; + + // --- 4. Final Color Assembly (opaque only; transmission handled in GlassPSMain) --- + float3 color = opaqueLit; + float alphaOut = baseColor.a; + + // Clip-plane discard during reflection render pass (to remove behind-plane geometry) + if (ubo.reflectionPass == 1) { + float side = dot(ubo.clipPlaneWS, float4(input.WorldPos, 1.0)); + if (side > 0.0) discard; // discard geometry on the positive side of the plane + } + + // Note: reflections are only applied in glass path (GlassPSMain). No planar reflection + // sampling here to avoid banding/aliasing and ensure user-requested behavior. + + // --- 5. Post-Processing --- + // Output linear color for intermediate buffers (composite pass will tonemap) + return float4(color, alphaOut); +} + +// Fragment shader entry point specialized for architectural glass. +// Shares the same inputs and bindings as PSMain, but uses a much simpler +// and more stable shading model: primarily refraction of the opaque scene +// with a small ambient/emissive surface term. Direct diffuse/specular +// lighting and screen-space reflections are omitted to avoid global +// bright/dark flashes across large glass surfaces. +[[shader("fragment")]] +float4 GlassPSMain(VSOutput input) : SV_TARGET +{ + // --- 1. Material / texture sampling (minimal subset) --- + float2 uv = float2(input.UV.x, 1.0 - input.UV.y); + + float4 baseColor = (material.baseColorTextureSet < 0) + ? material.baseColorFactor + : baseColorMap.Sample(uv) * material.baseColorFactor; + + // Emissive (same logic as PSMain) + float3 emissiveTex = (material.emissiveTextureSet < 0) + ? float3(1.0, 1.0, 1.0) + : emissiveMap.Sample(uv).rgb; + float3 emissiveFactor = material.emissiveFactor; + float3 emissive = emissiveTex * emissiveFactor; + if (material.hasEmissiveStrengthExt) + emissive *= material.emissiveStrength; + + // Alpha mask discard as in PSMain + if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { + discard; + } + + // Geometric normal for view-angle dependence and refraction + float3 G = normalize(input.GeometricNormal); + float3 V = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + + // Base albedo used for transmission tint + float3 albedo = baseColor.rgb; + + // Ambient is intentionally disabled for the glass path. + // Even small ambient terms can make large glass surfaces look "filled in" + // (frosted/opaque) rather than primarily showing the background through refraction. + + // Transmission factor from push constants. + // Some assets flag “glass” via engine-side heuristics but may not author + // `KHR_materials_transmission`. Since this shader is only used for glass, + // derive a robust effective transmission so glass never goes black. + float T_auth = clamp(material.transmissionFactor, 0.0, 1.0); + float opacity = clamp(baseColor.a, 0.0, 1.0); + float T_fromAlpha = 1.0 - opacity; + float T_eff = max(T_auth, T_fromAlpha); + if (T_eff < 0.01) { + // Default to mostly transmissive for glass when no explicit transmission/alpha is authored. + T_eff = 0.90; + } + + float3 color; + float alphaOut = baseColor.a; + + if (T_eff > 0.0) { + // Transmission/background sample (refraction approximation): sample the opaque scene behind glass. + float2 uvR = input.Position.xy / ubo.screenDimensions; + uvR = clamp(uvR, float2(0.0, 0.0), float2(1.0, 1.0)); + float3 bg = opaqueSceneColor.Sample(uvR).rgb; + // Tint the background by albedo to approximate colored glass. + bg *= lerp(float3(1.0, 1.0, 1.0), max(albedo, 0.6), 0.8); + + // Planar reflection sample (optional) + float3 refl = bg; + if (ubo.reflectionEnabled == 1) { + float4 pr = mul(ubo.reflectionVP, float4(input.WorldPos, 1.0)); + float2 uvP = pr.xy / max(pr.w, 1e-5); + uvP = uvP * 0.5 + 0.5; + if (uvP.x >= 0.0 && uvP.x <= 1.0 && uvP.y >= 0.0 && uvP.y <= 1.0) { + refl = reflectionMap.Sample(uvP).rgb; + } + } + + // Stylized, stable glass: Use a tinted + // glass body + rim highlight, then add planar reflection contribution. + + // Use symmetric |N·V| so that front/back views of thin glass walls + // behave consistently (important when looking down into glasses). + float NdotV = abs(dot(G, V)); + + // Base clear color from albedo, slightly dimmed so glass does not + // appear self-emissive. + float3 clearColor = albedo * 0.6; + + // Rim term stronger at grazing angles (1 - NdotV)^3, but keep it subtle + float edge = pow(1.0 - NdotV, 3.0); + float3 rimColor = lerp(clearColor, float3(1.0, 1.0, 1.0), 0.25); + + // Surface term: keep subtle so glass does not appear frosted. + float3 surfaceBase = emissive; + float3 surfaceTerm = surfaceBase * (1.0 - T_eff) * 0.12; + + // Base surface appearance (slight body + rim) and transmitted background. + float3 glassBody = clearColor * 0.08; + float3 rim = rimColor * (edge * 0.25); + float3 surface = glassBody + rim + surfaceTerm; + + // Primary transmission mix: this is what makes interior lighting visible through windows. + color = lerp(surface, bg, T_eff); + + // Restore Fresnel-blended mixing with boosted visibility for debugging/tuning. + float3 F_view2 = FresnelSchlick(NdotV, float3(0.06, 0.06, 0.06)); + float F_avg2 = (F_view2.r + F_view2.g + F_view2.b) / 3.0; + float reflStrength = saturate(0.20 + (1.5 * F_avg2) * (1.0 - material.roughnessFactor)); + // Scale by user-controlled intensity + reflStrength *= max(0.0, ubo.reflectionIntensity); + color = lerp(color, refl, reflStrength); + + // Fresnel influences alpha (how opaque the glass appears), not color here. + // We already used F to modulate reflection strength above. + + // Opacity model for architectural glass: mostly transparent at + // normal incidence, with a gentle Fresnel-driven increase in + // opacity toward grazing angles. TransmissionFactor controls how + // much of the underlying scene shows through overall. + + // Since we are sampling the background (opaqueSceneColor) and mixing it in the shader, + // we should output an alpha of 1.0 to ensure our mixed color is shown correctly + // in the swapchain, avoiding "double blending" with the hardware blender. + alphaOut = 1.0; + } else { + // Non-transmissive fallback: just ambient + emissive. + color = emissive; + } + + // Simple Forward+ lighting for glass (additive), using per-tile lists. + // This is a pragmatic lighting contribution so emissive bulbs can light glass-covered pixels. + // It does not model full transmission; it simply adds local diffuse+spec highlights. + { + const uint TILE = 16u; + uint tilesX = (uint(ubo.screenDimensions.x) + TILE - 1u) / TILE; + uint tilesY = (uint(ubo.screenDimensions.y) + TILE - 1u) / TILE; + uint px = (uint)max(0.0, input.Position.x); + uint py = (uint)max(0.0, input.Position.y); + uint tileX = (tilesX > 0u) ? min(px / TILE, tilesX - 1u) : 0u; + uint tileY = (tilesY > 0u) ? min(py / TILE, tilesY - 1u) : 0u; + uint totalTiles = max(tilesX * tilesY, 1u); + uint tileId = tileY * tilesX + tileX; + uint base = 0u; + uint count = 0u; + if (tileId < totalTiles) { + TileHeader th = tileHeaders[tileId]; + base = th.offset; + count = th.count; + } + if (count > 0u) { + float3 Ng = normalize(input.GeometricNormal); + float3 Vv = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + // Use a neutral albedo to avoid darkening glass; weight specular more + float3 alb = float3(0.6, 0.6, 0.6); + float rough = 0.49; + float metal = 0.0; + for (uint li = 0u; li < count; ++li) { + uint lightIndex = tileLightIndices[base + li]; + LightData light = lightBuffer[lightIndex]; + float3 L, radiance; + float distToLight = 10000.0; + if (light.lightType == 1) { + L = normalize(-light.position.xyz); + radiance = light.color.rgb; + } else { + float3 toLight = light.position.xyz - input.WorldPos; + float d = length(toLight); + L = (d > 1e-5) ? toLight / d : float3(0,0,1); + distToLight = d; + if (light.lightType == 3) { + float r = max(light.range, 0.001); + float att = 1.0 / (1.0 + (d / r) * (d / r)); + radiance = light.color.rgb * att; + } else { + radiance = light.color.rgb / max(d * d, 0.0001); + } + } + float rawDot = dot(Ng, L); + float NdotL = (light.lightType == 3) ? abs(rawDot) : max(rawDot, 0.0); + if (NdotL > 0.0) { + float visibility = 1.0; + if (ubo.padding2 != 0.0) { + float tMaxShadow = (light.lightType == 1) ? 10000.0 : max(distToLight - RASTER_SHADOW_EPS, RASTER_SHADOW_EPS); + float3 shadowOrigin = input.WorldPos + Ng * RASTER_SHADOW_EPS; + bool occluded = traceShadowOccluded(shadowOrigin, L, RASTER_SHADOW_EPS, tMaxShadow); + visibility = occluded ? 0.0 : 1.0; + } + + float3 H = normalize(Vv + L); + float NdotV = max(dot(Ng, Vv), 0.0); + float NdotH = max(dot(Ng, H), 0.0); + float HdotV = max(dot(H, Vv), 0.0); + float D = DistributionGGX(NdotH, rough); + float G = GeometrySmith(NdotV, NdotL, rough); + float3 F = FresnelSchlick(HdotV, lerp(float3(0.04,0.04,0.04), alb, metal)); + float3 spec = (D * G * F) / max(4.0 * NdotV * NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metal); + // Add a modest contribution to the glass color + color += ((kD * alb / PI) * radiance * NdotL * 0.6 + spec * radiance * NdotL * 0.8) * visibility; + } + } + } + } + + + // --- 3. Post-processing (same as PSMain) --- + color *= ubo.exposure; + + // Uncharted2 / Hable filmic tonemap. Use the canonical form without + // the extra 1.2 pre-scale so that midtones and shadows are not + // over-compressed relative to highlights. + float3 t = Hable_Filmic_Tonemapping::Uncharted2Tonemap(color); + float3 w = Hable_Filmic_Tonemapping::Uncharted2Tonemap(float3(1,1,1) * Hable_Filmic_Tonemapping::W); + color = t / max(w, float3(1e-6, 1e-6, 1e-6)); + + if (ubo.padding0 == 0) { + color = pow(saturate(color), float3(1.0 / ubo.gamma)); + } else { + color = saturate(color); + } + + return float4(color, alphaOut); +} diff --git a/attachments/openxr_engine/shaders/pbr_utils.slang b/attachments/openxr_engine/shaders/pbr_utils.slang new file mode 100644 index 000000000..59846a204 --- /dev/null +++ b/attachments/openxr_engine/shaders/pbr_utils.slang @@ -0,0 +1,55 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// PBR utility functions for physically-based rendering +// Shared between rasterization and ray query shaders + +import common_types; + +// GGX/Trowbridge-Reitz normal distribution function +// Describes the distribution of microfacet normals +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + return nom / max(denom, 0.000001); +} + +// Smith's geometry function with Schlick-GGX approximation +// Describes the self-shadowing of microfacets +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + return ggx1 * ggx2; +} + +// Fresnel-Schlick approximation +// Describes the ratio of reflected vs refracted light +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); +} + +// Fresnel for dielectric materials (given IOR) +float3 Fresnel_Dielectric(float cosTheta, float ior) { + float r0 = (1.0 - ior) / (1.0 + ior); + float3 F0 = float3(r0 * r0); + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); +} diff --git a/attachments/openxr_engine/shaders/physics.slang b/attachments/openxr_engine/shaders/physics.slang new file mode 100644 index 000000000..ce8c9e34b --- /dev/null +++ b/attachments/openxr_engine/shaders/physics.slang @@ -0,0 +1,460 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Compute shader for physics simulation +// This shader processes rigid body physics data to simulate physical interactions + + +// Physics data structure +struct PhysicsData { + float4 position; // xyz = position, w = inverse mass + float4 rotation; // quaternion + float4 linearVelocity; // xyz = velocity, w = restitution + float4 angularVelocity; // xyz = angular velocity, w = friction + float4 force; // xyz = force, w = is kinematic (0 or 1) + float4 torque; // xyz = torque, w = use gravity (0 or 1) + float4 colliderData; // type-specific data (e.g., radius for spheres) + float4 colliderData2; // additional collider data (e.g., box half extents) +}; + +// Collision data structure +struct CollisionData { + uint bodyA; + uint bodyB; + float4 contactNormal; // xyz = normal, w = penetration depth + float4 contactPoint; // xyz = contact point, w = unused +}; + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer physicsBuffer; // Physics data +[[vk::binding(1, 0)]] RWStructuredBuffer collisionBuffer; // Collision data +[[vk::binding(2, 0)]] RWStructuredBuffer pairBuffer; // Potential collision pairs +[[vk::binding(3, 0)]] RWStructuredBuffer counterBuffer; // [0] = pair count, [1] = collision count + +// Parameters for physics simulation +[[vk::binding(4, 0)]] ConstantBuffer params; + +struct PhysicsParams { + float deltaTime; // Time step - 4 bytes + uint numBodies; // Number of rigid bodies - 4 bytes + uint maxCollisions; // Maximum number of collisions - 4 bytes + float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes + float4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes + // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) +}; + +// Quaternion multiplication +float4 quatMul(float4 q1, float4 q2) { + return float4( + q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + ); +} + +// Quaternion normalization +float4 quatNormalize(float4 q) { + float len = length(q); + if (len > 0.0001) { + return q / len; + } + return float4(0, 0, 0, 1); +} + +// Integration shader - updates positions and velocities +[shader("compute")] +[numthreads(64, 1, 1)] +void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of bodies + if (index >= params.numBodies) { + return; + } + + // Get physics data for this body + PhysicsData body = physicsBuffer[index]; + + + // Skip kinematic bodies + if (body.force.w > 0.5) { + return; + } + + // Apply gravity if enabled + if (body.torque.w > 0.5) { + float3 gravityForce = params.gravity.xyz * body.position.w; + body.force.xyz += gravityForce; + + } + + // Integrate forces + float3 velocityChange = body.force.xyz * body.position.w * params.deltaTime; + body.linearVelocity.xyz += velocityChange; + body.angularVelocity.xyz += body.torque.xyz * params.deltaTime; // Simplified, should use inertia tensor + + + // Apply damping + const float linearDamping = 0.01; + const float angularDamping = 0.01; + body.linearVelocity.xyz *= (1.0 - linearDamping); + body.angularVelocity.xyz *= (1.0 - angularDamping); + + // Integrate velocities + float3 positionChange = body.linearVelocity.xyz * params.deltaTime; + body.position.xyz += positionChange; + + + // Update rotation + float4 angularVelocityQuat = float4(body.angularVelocity.xyz * 0.5, 0.0); + float4 rotationDelta = quatMul(angularVelocityQuat, body.rotation); + body.rotation = quatNormalize(body.rotation + rotationDelta * params.deltaTime); + + // Write updated data back to buffer + physicsBuffer[index] = body; + +} + +// Compute AABB for a body +void computeAABB(PhysicsData body, out float3 min, out float3 max) { + // Default to a small AABB + min = body.position.xyz - float3(0.1, 0.1, 0.1); + max = body.position.xyz + float3(0.1, 0.1, 0.1); + + // Check collider type + int colliderType = int(body.colliderData.w); + + if (colliderType == 0) { // Sphere + float radius = body.colliderData.x; + float3 center = body.position.xyz + body.colliderData2.xyz; + min = center - float3(radius, radius, radius); + max = center + float3(radius, radius, radius); + } + else if (colliderType == 1) { // Box + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } + else if (colliderType == 2) { // Mesh (represented as large bounding box) + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } +} + +// Check if two AABBs overlap +bool aabbOverlap(float3 minA, float3 maxA, float3 minB, float3 maxB) { + return all(minA < maxB) && all(minB < maxA); +} + +// Broad phase collision detection - identifies potential collision pairs +[shader("compute")] +[numthreads(64, 1, 1)] +void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Calculate total number of pairs + uint numPairs = (params.numBodies * (params.numBodies - 1)) / 2; + + if (index >= numPairs) { + return; + } + + // Convert linear index to pair indices (i, j) where i < j + // Use a more robust algorithm that avoids floating-point precision issues + uint i = 0; + uint j = 0; + + // Find i and j using integer arithmetic to avoid precision errors + uint remaining = index; + uint currentRow = 0; + + // Find which row (i value) this index belongs to + while (remaining >= (params.numBodies - 1 - currentRow)) { + remaining -= (params.numBodies - 1 - currentRow); + currentRow++; + } + + i = currentRow; + j = i + 1 + remaining; + + // Get physics data for both bodies + PhysicsData bodyA = physicsBuffer[i]; + PhysicsData bodyB = physicsBuffer[j]; + + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Skip if either body doesn't have a collider + if (bodyA.colliderData.w < 0 || bodyB.colliderData.w < 0) { + return; + } + + // Early culling: only consider pairs where at least one body is a sphere (shape 0) + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + if (!(shapeA == 0 || shapeB == 0)) { + return; + } + + // Compute AABBs + float3 minA, maxA, minB, maxB; + computeAABB(bodyA, minA, maxA); + computeAABB(bodyB, minB, maxB); + + // Expand sphere AABBs by motion over the timestep to catch fast-moving spheres + if (shapeA == 0) { + float3 expandA = abs(bodyA.linearVelocity.xyz) * params.deltaTime; + minA -= expandA; maxA += expandA; + } + if (shapeB == 0) { + float3 expandB = abs(bodyB.linearVelocity.xyz) * params.deltaTime; + minB -= expandB; maxB += expandB; + } + + // Check for AABB overlap + if (aabbOverlap(minA, maxA, minB, maxB)) { + // Add to potential collision pairs + uint pairIndex; + InterlockedAdd(counterBuffer[0], 1, pairIndex); + + if (pairIndex < params.maxCollisions) { + pairBuffer[pairIndex] = uint2(i, j); + } + } +} + +// Narrow phase collision detection - detailed collision detection for potential pairs +[shader("compute")] +[numthreads(64, 1, 1)] +void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of potential pairs + uint numPairs = counterBuffer[0]; + if (index >= numPairs || index >= params.maxCollisions) { + return; + } + + // Get the pair of bodies + uint2 pair = pairBuffer[index]; + uint bodyIndexA = pair.x; + uint bodyIndexB = pair.y; + + PhysicsData bodyA = physicsBuffer[bodyIndexA]; + PhysicsData bodyB = physicsBuffer[bodyIndexB]; + + // Determine collision shapes + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + + // Handle sphere-sphere collisions + if (shapeA == 0 && shapeB == 0) { // Both are spheres + float radiusA = bodyA.colliderData.x; + float radiusB = bodyB.colliderData.x; + + float3 posA = bodyA.position.xyz + bodyA.colliderData2.xyz; + float3 posB = bodyB.position.xyz + bodyB.colliderData2.xyz; + + float3 direction = posB - posA; + float distance = length(direction); + float minDistance = radiusA + radiusB; + + if (distance < minDistance) { + // Collision detected + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Normalize direction + float3 normal = direction / max(distance, 0.0001); + + // Create collision data + CollisionData collision; + collision.bodyA = bodyIndexA; + collision.bodyB = bodyIndexB; + collision.contactNormal = float4(normal, minDistance - distance); // penetration depth + collision.contactPoint = float4(posA + normal * radiusA, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } + } + // Handle sphere-geometry collisions (sphere vs mesh represented as box) + else if ((shapeA == 0 && shapeB == 2) || (shapeA == 2 && shapeB == 0)) { + // Determine which is sphere and which is geometry + PhysicsData sphere = (shapeA == 0) ? bodyA : bodyB; + PhysicsData geometry = (shapeA == 0) ? bodyB : bodyA; + uint sphereIndex = (shapeA == 0) ? bodyIndexA : bodyIndexB; + uint geometryIndex = (shapeA == 0) ? bodyIndexB : bodyIndexA; + + float sphereRadius = sphere.colliderData.x; + float3 spherePos = sphere.position.xyz + sphere.colliderData2.xyz; + float3 geometryPos = geometry.position.xyz + geometry.colliderData2.xyz; + float3 geometryHalfExtents = geometry.colliderData.xyz; + + // Simple sphere-box collision detection + float3 closestPoint = clamp(spherePos, geometryPos - geometryHalfExtents, geometryPos + geometryHalfExtents); + float3 direction = spherePos - closestPoint; + float distance = length(direction); + + if (distance < sphereRadius) { + // Collision detected (overlap) + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Calculate normal so that it points from sphere(A) to geometry(B) + float3 normal = (distance > 0.0001) ? (-direction / distance) : float3(0, 1, 0); + float penetration = sphereRadius - distance; + + // Create collision data + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + collision.contactNormal = float4(normal, penetration); + collision.contactPoint = float4(closestPoint, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } else { + // Swept test (CCD-lite): segment from previous position to current against box expanded by sphere radius + float3 prevPos = spherePos - sphere.linearVelocity.xyz * params.deltaTime; + float3 dir = spherePos - prevPos; + float dirLen = length(dir); + if (dirLen > 1e-6) { + float3 bbMin = geometryPos - (geometryHalfExtents + sphereRadius); + float3 bbMax = geometryPos + (geometryHalfExtents + sphereRadius); + + float3 invDir = 1.0 / max(abs(dir), float3(1e-6, 1e-6, 1e-6)); + float3 t0 = (bbMin - prevPos) / dir; + float3 t1 = (bbMax - prevPos) / dir; + float3 tmin3 = min(t0, t1); + float3 tmax3 = max(t0, t1); + float tEnter = max(tmin3.x, max(tmin3.y, tmin3.z)); + float tExit = min(tmax3.x, min(tmax3.y, tmax3.z)); + + if (tEnter >= 0.0 && tEnter <= 1.0 && tEnter <= tExit) { + // Determine contact normal based on entry axis and direction of motion + float3 normal = float3(0,0,0); + if (tEnter >= tmin3.x && tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3((dir.x > 0.0) ? 1.0 : -1.0, 0.0, 0.0); + } else if (tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3(0.0, (dir.y > 0.0) ? 1.0 : -1.0, 0.0); + } else { + normal = float3(0.0, 0.0, (dir.z > 0.0) ? 1.0 : -1.0); + } + + float3 hitPoint = prevPos + dir * tEnter; + + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + if (collisionIndex < params.maxCollisions) { + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + // Tiny penetration to trigger resolution without large positional correction + collision.contactNormal = float4(normalize(normal), 0.0); + collision.contactPoint = float4(hitPoint, 0.0); + collisionBuffer[collisionIndex] = collision; + } + } + } + } + } +} + +// Collision resolution - resolves detected collisions +[shader("compute")] +[numthreads(64, 1, 1)] +void ResolveCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of collisions + uint numCollisions = counterBuffer[1]; + if (index >= numCollisions || index >= params.maxCollisions) { + return; + } + + // Get collision data + CollisionData collision = collisionBuffer[index]; + + // Get the bodies involved in the collision + PhysicsData bodyA = physicsBuffer[collision.bodyA]; + PhysicsData bodyB = physicsBuffer[collision.bodyB]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Calculate relative velocity + float3 relativeVelocity = bodyB.linearVelocity.xyz - bodyA.linearVelocity.xyz; + + // Calculate velocity along normal + float velocityAlongNormal = dot(relativeVelocity, collision.contactNormal.xyz); + + // Don't resolve if velocities are separating + if (velocityAlongNormal > 0) { + return; + } + + // Calculate restitution (bounciness) + float restitution = min(bodyA.linearVelocity.w, bodyB.linearVelocity.w); + + // Calculate impulse scalar + float j = -(1.0 + restitution) * velocityAlongNormal; + j /= bodyA.position.w + bodyB.position.w; + + // Apply impulse + float3 impulse = collision.contactNormal.xyz * j; + + // Update velocities + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.linearVelocity.xyz -= impulse * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.linearVelocity.xyz += impulse * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } + + // Position correction to prevent sinking + const float percent = 0.2; // usually 20% to 80% + const float slop = 0.01; // small penetration allowed + float3 correction = max(collision.contactNormal.w - slop, 0.0) * percent * collision.contactNormal.xyz / (bodyA.position.w + bodyB.position.w); + + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.position.xyz -= correction * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.position.xyz += correction * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } +} diff --git a/attachments/openxr_engine/shaders/ray_query.slang b/attachments/openxr_engine/shaders/ray_query.slang new file mode 100644 index 000000000..ad09434cc --- /dev/null +++ b/attachments/openxr_engine/shaders/ray_query.slang @@ -0,0 +1,1067 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Ray query compute shader for ray-traced rendering +// Uses Slang's ray query extension as an alternative to rasterization + +import common_types; +import pbr_utils; +import lighting_utils; +import tonemapping_utils; + +// C++ Vertex structure layout (tightly packed, 48 bytes total): +// - position: vec3 at offset 0 (12 bytes) +// - normal: vec3 at offset 12 (12 bytes) +// - texCoord: vec2 at offset 24 (8 bytes) +// - tangent: vec4 at offset 32 (16 bytes) +// We'll read normals directly as floats to avoid PhysicalStorageBuffer alignment issues + +// Ray Query uses a dedicated uniform layout to avoid CPU↔shader drift. +// IMPORTANT: This must match `RayQueryUniformBufferObject` in `renderer.h`. +struct RayQueryUniforms { + [[vk::offset(0)]] float4x4 model; + [[vk::offset(64)]] float4x4 view; + [[vk::offset(128)]] float4x4 proj; + [[vk::offset(192)]] float4 camPos; + + [[vk::offset(208)]] float exposure; + [[vk::offset(212)]] float gamma; + [[vk::offset(216)]] float scaleIBLAmbient; + [[vk::offset(220)]] int lightCount; + [[vk::offset(224)]] int enableRayQueryReflections; + [[vk::offset(228)]] int enableRayQueryTransparency; + + [[vk::offset(232)]] float2 screenDimensions; + [[vk::offset(240)]] int geometryInfoCount; + [[vk::offset(244)]] int materialCount; + [[vk::offset(248)]] int _pad0; + [[vk::offset(252)]] int enableThickGlass; // 0/1 toggle for thick-glass attenuation + [[vk::offset(256)]] float thicknessClamp; // max thickness in meters (safety clamp) + [[vk::offset(260)]] float absorptionScale; // scales sigma_a (1=as-is) + // Ray Query shadows: 0/1 enable (wired from C++ as `enableRayQueryShadows`) + [[vk::offset(264)]] int _pad1; + // Ray Query soft shadows (area-light approximation) + [[vk::offset(268)]] int shadowSampleCount; // 1 = hard shadows (single shadow ray) + [[vk::offset(272)]] float shadowSoftness; // 0 = hard; otherwise scales effective light radius (see shader) + [[vk::offset(276)]] float reflectionIntensity; // user control + [[vk::offset(280)]] float _padShadow1; + [[vk::offset(284)]] float _padShadow2; +}; + +// Compute shader descriptor bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] RaytracingAccelerationStructure tlas; // Top-level acceleration structure +[[vk::binding(2, 0)]] RWTexture2D outputImage; // Output render target +[[vk::binding(3, 0)]] StructuredBuffer lightBuffer; +[[vk::binding(4, 0)]] StructuredBuffer geometryInfoBuffer; // Geometry vertex/index addresses +[[vk::binding(5, 0)]] StructuredBuffer materialBuffer; // Material properties +// Fixed-size Ray Query texture table (combined image samplers) +// Must match Renderer::RQ_MAX_TEX in C++ (currently 2048) +static const uint RQ_MAX_TEX = 2048; +[[vk::binding(6, 0)]] Sampler2D baseColorTex[RQ_MAX_TEX]; + +// (No debug buffer in production layout) + +// (No debug toggles in production) + +// NOTE: Ray Query debugPrintf/printf diagnostics are intentionally removed. +// Keep Ray Query VVL-clean and log-noise-free by default. + +float3 materialDebugColor(uint materialIndex) { + float u = float(materialIndex + 1); + float3 h = frac(float3(0.06711056, 0.00583715, 0.2065) * u); + return frac(h * 2.6180339); +} + +// Simple ray-scene intersection result +struct HitInfo { + bool hit; + float t; + float3 worldPos; + float3 normal; + float3 baseColor; // base color (factor * texture), not lit + float3 color; + float3 F0; + float roughness; + float metallic; + float transmission; // Added for glass/transparency support + float ior; + bool isGlass; // Material is glass (should always have reflections/transparency) + bool isLiquid; + bool thinWalled; // true = thin surface (no thickness), false = thick volume + int alphaMode; // 0=OPAQUE, 1=MASK, 2=BLEND + float opacity; // 0..1, derived from baseColor alpha (factor * texture) + uint instanceId; // Committed instance ID (for debug coloring) + float2 uv; // Interpolated UV at the hit (glTF V flip already applied) + uint texIndex; // Resolved baseColor texture index used for sampling (for stats/debug) + uint materialIndex; // Resolved/clamped material index used +}; + +static const float RQ_RAY_EPS = 0.002; + +uint rqHash(uint v) +{ + // Thomas Wang 32-bit integer hash + v = (v ^ 61u) ^ (v >> 16u); + v *= 9u; + v = v ^ (v >> 4u); + v *= 0x27d4eb2du; + v = v ^ (v >> 15u); + return v; +} + +float rqRand01(inout uint state) +{ + state = rqHash(state); + // 24-bit mantissa-ish + return float(state & 0x00FFFFFFu) * (1.0 / 16777216.0); +} + +float2 rqSampleDisk(inout uint state) +{ + // Uniform disk sampling + float u1 = rqRand01(state); + float u2 = rqRand01(state); + float r = sqrt(max(u1, 0.0)); + float a = 6.28318530718 * u2; + return float2(cos(a), sin(a)) * r; +} + +float3 skyColor(float3 dir) { + float t = saturate(0.5 * (dir.y + 1.0)); + float3 a = float3(0.06, 0.06, 0.09); + float3 b = float3(0.20, 0.25, 0.32); + return lerp(a, b, t); +} + +// Heuristic explicit-LOD for compute sampling (no implicit derivatives available) +float computeTextureLOD(float3 worldPos, float roughness) { + // Approximate screen-space footprint from view distance and modulate by roughness + float d = length(ubo.camPos.xyz - worldPos); + // Tune 0.25 scale empirically for this scene; avoids over-sharp minification + float lod = log2(max(d * 0.25, 1.0)); + lod *= lerp(0.6, 1.4, saturate(roughness)); + return max(lod, 0.0); +} + +float3 shadeWithSecondaryRays(float3 rayOrigin, float3 rayDir, HitInfo hit, inout uint rngState) { + float3 base = max(hit.color, 0.0); + int maxBounces = clamp(ubo._pad0, 0, 10); + + float3 N = hit.normal; + float3 V = normalize(-rayDir); + float NdotV = abs(dot(N, V)); + + // Fresnel for reflection weighting + float3 F = FresnelSchlick(NdotV, hit.F0); + if (hit.transmission > 0.01 || hit.isGlass) { + F = Fresnel_Dielectric(NdotV, hit.ior); + } + float Fr = saturate((F.r + F.g + F.b) * (1.0 / 3.0)); + + // Transmission gate + float opacity = clamp(hit.opacity, 0.0, 1.0); + float blendTransmission = (hit.alphaMode == 2) ? (1.0 - opacity) : 0.0; + float physicalTransmission = clamp(hit.transmission, 0.0, 1.0); + + // Many scenes tag architectural glass via a material hint (`hit.isGlass`) even when + // `KHR_materials_transmission` is not authored. Provide a robust fallback so glass + // does not turn black and so interior lighting remains visible through windows. + float glassTransmission = 0.0; + if (hit.isGlass) { + // Only apply the strong "glass fallback" when the surface actually looks like a pane + // (i.e., has meaningful coverage/opacity < 1). This avoids pushing opaque surfaces that + // were heuristically tagged as glass into the refraction path (which can cause view-dependent banding). + glassTransmission = max(physicalTransmission, (1.0 - opacity)); + if (glassTransmission < 0.01 && opacity < 0.98) { + glassTransmission = 0.90; + } + } + + // Alpha BLEND (decals/foliage) should work regardless of the "transparency/refraction" toggle. + // That toggle is intended for true transmission/glass, not basic alpha compositing. + float T = blendTransmission; + if (ubo.enableRayQueryTransparency != 0 && !hit.isLiquid) { + T = max(T, max(physicalTransmission, glassTransmission)); + } + + // Reflection ray (chain up to maxBounces) + float3 reflCol = float3(0.0, 0.0, 0.0); + bool doRefl = (ubo.enableRayQueryReflections != 0) && (Fr > 1e-4); + if (doRefl) { + float3 ro = hit.worldPos + hit.normal * RQ_RAY_EPS; + float3 rd = normalize(reflect(rayDir, N)); + + float3 last = skyColor(rd); + for (int b = 0; b < maxBounces; ++b) { + HitInfo rh = traceRay(ro, rd, RQ_RAY_EPS, 10000.0, rngState); + last = rh.hit ? max(rh.color, 0.0) : skyColor(rd); + if (!rh.hit) { + break; + } + // Next bounce + float3 Nr = normalize(rh.normal); + rd = normalize(reflect(rd, Nr)); + ro = rh.worldPos + Nr * RQ_RAY_EPS; + } + reflCol = last * ubo.reflectionIntensity; + } + + // Alpha BLEND (decals/foliage) is coverage. Composite over what is behind along the same ray. + // Do this iteratively (no recursion allowed in SPIR-V codegen). + if (hit.alphaMode == 2 && physicalTransmission <= 1e-4 && !hit.isGlass) { + float3 accum = float3(0.0, 0.0, 0.0); + float trans = 1.0; + HitInfo cur = hit; + + // Composite up to a small number of BLEND layers (coplanar decals). + // After that, treat the next hit as opaque background. + uint lastInst = cur.instanceId; + uint lastMat = cur.materialIndex; + uint lastPrim = 0u; + + [loop] + for (int layer = 0; layer < 8; ++layer) { + float a = clamp(cur.opacity, 0.0, 1.0); + accum += max(cur.color, 0.0) * (a * trans); + trans *= (1.0 - a); + if (trans < 1e-3) { + break; + } + + // Step behind the current BLEND layer. Coplanar decals can self-hit even with a small bias, + // which would cause us to repeatedly composite the same layer and look opaque. + float3 startPos = cur.worldPos + cur.normal * (4.0 * RQ_RAY_EPS) + rayDir * (4.0 * RQ_RAY_EPS); + HitInfo next = traceRay(startPos, rayDir, RQ_RAY_EPS, 10000.0, rngState); + + // If we re-hit the same instance/material, push further along the ray direction and retry a few times. + [loop] + for (int retry = 0; retry < 4 && next.hit; ++retry) { + if (next.instanceId != lastInst || next.materialIndex != lastMat) { + break; + } + startPos += rayDir * (16.0 * RQ_RAY_EPS); + next = traceRay(startPos, rayDir, RQ_RAY_EPS, 10000.0, rngState); + } + if (!next.hit) { + accum += skyColor(rayDir) * trans; + break; + } + + // Stop stacking when we reach a non-BLEND surface (or true transmission/glass). + if (next.alphaMode != 2 || next.isGlass || next.transmission > 0.01) { + accum += max(next.color, 0.0) * trans; + break; + } + + lastInst = next.instanceId; + lastMat = next.materialIndex; + cur = next; + } + return accum; + } + + // Transmission ray (iterative loop to support multiple layers) + float3 thruCol = skyColor(rayDir); + bool doThru = (T > 1e-4); + if (doThru) { + float3 curRd = rayDir; + float3 curRo = hit.worldPos; + float3 accumTint = float3(1.0); + HitInfo curHit = hit; + + for (int bounce = 0; bounce < maxBounces + 1; ++bounce) { + float curPhysT = clamp(curHit.transmission, 0.0, 1.0); + float curOpacity = clamp(curHit.opacity, 0.0, 1.0); + float curGlassT = curHit.isGlass ? max(curPhysT, 1.0 - curOpacity) : 0.0; + if (curHit.isGlass && curGlassT < 0.01 && curOpacity < 0.98) curGlassT = 0.9; + float curT = max(curPhysT, max((curHit.alphaMode == 2 ? 1.0 - curOpacity : 0.0), curGlassT)); + + if (curT < 1e-4) { + thruCol = accumTint * curHit.color; + break; + } + + float3 nextRd = curRd; + bool refracts = (curPhysT > 1e-4 || (curHit.isGlass && curOpacity < 0.98)); + if (refracts) { + float3 Nn = curHit.normal; + float eta = 1.0 / max(curHit.ior, 1.0); + // Determine if we're entering or exiting based on ray direction vs normal + if (dot(curRd, curHit.normal) > 0.0) { + Nn = -curHit.normal; + eta = max(curHit.ior, 1.0); + } + float3 refrDir; + if (refract(curRd, Nn, eta, refrDir)) { + nextRd = normalize(refrDir); + } else { + // Total internal reflection + thruCol = float3(0, 0, 0); + break; + } + } + + // Accumulate tint for refractive surfaces + if (refracts) { + float3 tint = max(clamp(curHit.baseColor, 0.0, 1.0), float3(0.5, 0.5, 0.5)); + accumTint *= tint; + } + + // Trace from just inside the entry point to reduce self-hits. + float3 startPos = curHit.worldPos + nextRd * (4.0 * RQ_RAY_EPS); + // For coplanar BLEND decals, stepping only along the ray direction can still re-hit + // the same decal layer due to precision. Push along the surface normal as well so the + // next trace lands on the underlying wall. + if (curHit.alphaMode == 2) { + startPos = curHit.worldPos + curHit.normal * (4.0 * RQ_RAY_EPS) + nextRd * (4.0 * RQ_RAY_EPS); + } + + // Volumetric absorption for THICK glass (skip for thin-walled) + if (ubo.enableThickGlass != 0 && !curHit.thinWalled && refracts) { + HitInfo exitHit = traceRay(startPos, nextRd, (4.0 * RQ_RAY_EPS), 10000.0, rngState); + bool haveExitSameSurface = exitHit.hit && + (exitHit.instanceId == curHit.instanceId) && + (exitHit.materialIndex == curHit.materialIndex); + if (haveExitSameSurface) { + float thickness = min(distance(exitHit.worldPos, curHit.worldPos), max(0.0, ubo.thicknessClamp)); + if (thickness > 1e-6) { + uint mi = min(curHit.materialIndex, (uint)max(0, ubo.materialCount-1)); + MaterialData m = materialBuffer[mi]; + float3 C = saturate(m.absorptionColor); + float D = max(m.absorptionDistance, 1e-4); + float3 sigma_a = -log(max(C, 1e-3)) / D; + sigma_a *= max(ubo.absorptionScale, 0.0); + accumTint *= saturate(exp(-sigma_a * thickness)); + } + startPos = exitHit.worldPos + nextRd * (4.0 * RQ_RAY_EPS); + } + } + + // Trace the scene beyond this layer + HitInfo nextHit = traceRay(startPos, nextRd, RQ_RAY_EPS, 10000.0, rngState); + if (!nextHit.hit) { + thruCol = accumTint * skyColor(nextRd); + break; + } + + // Assume opaque for now; check if we should continue looping + thruCol = accumTint * nextHit.color; + + float nPhysT = clamp(nextHit.transmission, 0.0, 1.0); + float nOpacity = clamp(nextHit.opacity, 0.0, 1.0); + float nGlassT = nextHit.isGlass ? max(nPhysT, 1.0 - nOpacity) : 0.0; + if (nextHit.isGlass && nGlassT < 0.01) nGlassT = 0.9; + float nT = max(nPhysT, max((nextHit.alphaMode == 2 ? 1.0 - nOpacity : 0.0), nGlassT)); + + // Stop if the next hit is opaque or we reached max bounces + if (nT < 1e-4 || bounce == maxBounces) { + break; + } + + // Refractive radiance compensation (mitigate amplification) + if (refracts) { + bool entering = (dot(curRd, curHit.normal) < 0.0); + float eta_ratio = entering ? (1.0 / max(curHit.ior, 1.0)) : max(curHit.ior, 1.0); + if (entering) { + float invEta2 = 1.0 / max(eta_ratio * eta_ratio, 1e-4); + accumTint *= clamp(invEta2, 0.0, 1.0); + } + } + + // Move to the next transmissive layer + curHit = nextHit; + curRd = nextRd; + } + } + + if (T > 1e-4) { + // Transmissive/glass: energy-conserving mix using Fresnel reflectance Fr and + // transmission factor T (authored/heuristic). Transmission weight Ft = (1-Fr)*T. + float Fr_s = saturate(Fr); + float Ft = saturate((1.0 - Fr_s) * T); + float sum = Fr_s + Ft; + if (sum > 1.0) { + // Normalize to avoid accidental gain (should rarely trigger) + Fr_s /= sum; + Ft /= sum; + } + + float3 mixed = Fr_s * reflCol + Ft * thruCol; + + // Glass/transmission path: return the energy-conserving mixture directly, + // plus direct surface highlights (specular + emissive) from the glass surface. + return mixed + base; + } + + // Opaque: add a controlled reflection contribution (avoids double-counting too much) + float reflWeight = doRefl ? (Fr * (1.0 - clamp(hit.roughness, 0.0, 1.0))) : 0.0; + return lerp(base, reflCol, reflWeight); +} + +float2 computeTriangleUV(uint instIndex, uint primitiveIndex, float2 bary) { + float3 barycentrics = float3(1.0 - bary.x - bary.y, bary.x, bary.y); + if (instIndex >= uint(max(0, ubo.geometryInfoCount))) return float2(0.0, 0.0); + GeometryInfo geoInfo = geometryInfoBuffer[instIndex]; + if (geoInfo.vertexBufferAddress == 0 || geoInfo.indexBufferAddress == 0) return float2(0.0, 0.0); + + uint triCount = geoInfo.indexCount / 3u; + if (primitiveIndex >= triCount) return float2(0.0, 0.0); + + uint* indexBuffer = (uint*)geoInfo.indexBufferAddress; + float* vertexBuffer = (float*)geoInfo.vertexBufferAddress; + uint idxBase = primitiveIndex * 3u; + uint i0 = indexBuffer[idxBase + 0u]; + uint i1 = indexBuffer[idxBase + 1u]; + uint i2 = indexBuffer[idxBase + 2u]; + if (i0 >= geoInfo.vertexCount || i1 >= geoInfo.vertexCount || i2 >= geoInfo.vertexCount) return float2(0.0, 0.0); + + uint vertexStride = 12; // floats + float2 uv0 = float2(vertexBuffer[i0 * vertexStride + 6], vertexBuffer[i0 * vertexStride + 7]); + float2 uv1 = float2(vertexBuffer[i1 * vertexStride + 6], vertexBuffer[i1 * vertexStride + 7]); + float2 uv2 = float2(vertexBuffer[i2 * vertexStride + 6], vertexBuffer[i2 * vertexStride + 7]); + float2 uv = uv0 * barycentrics.x + uv1 * barycentrics.y + uv2 * barycentrics.z; + uv.y = 1.0 - uv.y; // flip V for glTF + return uv; +} + +float computeBaseColorAlpha(MaterialData material, uint instIndex, uint primitiveIndex, float2 bary) { + // In the Ray Query pipeline, `baseColorTexIndex` is always valid (real texture or a default slot). + // Some assets rely on alpha in the baseColor texture even when CPU-side `baseColorTextureSet` + // can be incorrect/stale. For alpha-sensitive materials, sample via the texture-table index. + float alpha = material.alpha; + bool alphaSensitive = (material.alphaMode != 0) || (material.alpha < 0.999); + if (material.baseColorTextureSet >= 0 || alphaSensitive) { + float2 uv = computeTriangleUV(instIndex, primitiveIndex, bary); + uint tiBase = (uint)min(max(material.baseColorTexIndex, 0), int(RQ_MAX_TEX - 1)); + float4 baseColor = baseColorTex[NonUniformResourceIndex(tiBase)].SampleLevel(uv, 0.0); + baseColor *= float4(material.albedo, material.alpha); + alpha = baseColor.a; + } + return alpha; +} + +// Shadow query: returns true when there is an occluder between origin and tMax along direction. +// - MASK materials are alpha-tested against alphaCutoff. +// - BLEND materials are treated as non-occluding for shadows. +bool traceShadowOccluded(float3 origin, float3 direction, float tMin, float tMax) { + RayDesc ray; + ray.Origin = origin; + ray.Direction = direction; + ray.TMin = tMin; + ray.TMax = tMax; + + RayQuery q; + uint mask = 0xFF; + // Force non-opaque so we can decide in-shader whether a candidate occludes. + // This is required so transmissive / glass materials do not fully block shadow rays. + q.TraceRayInline( + tlas, + RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_FORCE_NON_OPAQUE, + mask, + ray + ); + + int maxIterations = 1000; + int iteration = 0; + while (q.Proceed() && iteration < maxIterations) { + iteration++; + + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { + uint instIndex = q.CandidateInstanceID(); + if (instIndex < uint(max(0, ubo.geometryInfoCount))) { + GeometryInfo geoInfoC = geometryInfoBuffer[instIndex]; + uint materialIndexC = 0u; + if (ubo.materialCount > 0) { + materialIndexC = min(geoInfoC.materialIndex, (uint)(ubo.materialCount - 1)); + } + MaterialData matC = materialBuffer[materialIndexC]; + + bool accept = true; + + // Treat transmissive surfaces as non-occluding for shadows. + // NOTE: This includes "opaque-but-glass" materials (alphaMode==OPAQUE with isGlass hint). + bool transmissive = (matC.isGlass != 0) || (matC.transmissionFactor > 0.01); + if (transmissive || matC.alphaMode == 2) { + accept = false; + } else if (matC.alphaMode == 1) { + float alpha = computeBaseColorAlpha(matC, instIndex, q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics()); + accept = (alpha >= matC.alphaCutoff); + } + + if (accept) { + q.CommitNonOpaqueTriangleHit(); + break; + } + } + } + } + + return (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT); +} + +// Calculate refraction direction using Snell's law +// Returns true if refraction occurs, false if total internal reflection +bool refract(float3 I, float3 N, float eta, out float3 refracted) { + float cosi = -dot(N, I); + float cost2 = 1.0 - eta * eta * (1.0 - cosi * cosi); + if (cost2 > 0.0) { + refracted = eta * I + (eta * cosi - sqrt(cost2)) * N; + return true; + } + refracted = float3(0, 0, 0); // Initialize even on failure + return false; // Total internal reflection +} + +// Perform ray query and return hit information with proper vertex normals and material properties +HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax, inout uint rngState) { + HitInfo result; + result.hit = false; + result.t = tMax; + result.uv = float2(0.0, 0.0); + result.texIndex = 0u; + result.materialIndex = 0u; + result.baseColor = float3(1.0, 1.0, 1.0); + result.color = float3(0.0, 0.0, 0.0); + result.alphaMode = 0; + result.opacity = 1.0; + + // Create ray query object + RayDesc ray; + ray.Origin = origin; + ray.Direction = direction; + ray.TMin = tMin; + ray.TMax = tMax; + + // Initialize ray query. + // We do NOT force opaque because we need to alpha-test MASK materials in-shader + // (RayQuery inline traversal has no any-hit shader). + RayQuery q; + uint primaryMask = 0xFF; + q.TraceRayInline( + tlas, + RAY_FLAG_NONE, + primaryMask, + ray + ); + + // Process ray query - loop until Proceed() returns false + // For opaque geometry, this finds the closest hit automatically + // Add safety limit to prevent infinite loops + int maxIterations = 1000; + int iteration = 0; + while (q.Proceed() && iteration < maxIterations) { + iteration++; + + // Alpha-mask handling: emulate any-hit by inspecting candidate triangle alpha + // and committing only when the candidate passes alpha test. + // Slang/HLSL ray query candidate type for triangle intersections. + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) { + uint instIndex = q.CandidateInstanceID(); + if (instIndex < uint(max(0, ubo.geometryInfoCount))) { + GeometryInfo geoInfoC = geometryInfoBuffer[instIndex]; + uint materialIndexC = 0u; + if (ubo.materialCount > 0) { + materialIndexC = min(geoInfoC.materialIndex, (uint)(ubo.materialCount - 1)); + } + MaterialData matC = materialBuffer[materialIndexC]; + + float alpha = computeBaseColorAlpha(matC, instIndex, q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics()); + + bool accept = true; + if (matC.alphaMode == 1) { + // MASK: classic cutout + accept = (alpha >= matC.alphaCutoff); + } else if (matC.alphaMode == 2) { + // BLEND: do not commit fully-transparent pixels. + // Committing alpha==0 hits can make opaque geometry behind decals disappear. + // Use a small threshold to avoid precision noise. + accept = (alpha > 0.01); + } + + if (accept) { + q.CommitNonOpaqueTriangleHit(); + } + } + } + } + + // Check if we hit anything + if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { + result.hit = true; + result.t = q.CommittedRayT(); + result.worldPos = origin + direction * result.t; + // Use CommittedInstanceID() which returns the instance custom index we set on CPU + // (our per-instance sequential index that matches geometryInfoBuffer order). + uint instIndex = q.CommittedInstanceID(); + result.instanceId = instIndex; + + // Get barycentric coordinates + float2 bary = q.CommittedTriangleBarycentrics(); + float3 barycentrics = float3(1.0 - bary.x - bary.y, bary.x, bary.y); + + // PROPER GEOMETRY DATA FETCHING WITH SAFETY CHECKS + // Per-instance geometry info index equals the instance custom index we assigned on CPU. + uint blasIndex = instIndex; + uint primitiveIndex = q.CommittedPrimitiveIndex(); + + // Validate instance index is in bounds of geometry info buffer + if (blasIndex >= uint(max(0, ubo.geometryInfoCount))) { + // Invalid BLAS index, use default values + result.normal = float3(0, 1, 0); + result.baseColor = float3(0.8, 0.8, 0.8); + result.color = float3(0.8, 0.8, 0.8); + result.metallic = 0.0; + result.roughness = 0.5; + result.transmission = 0.0; + result.isGlass = false; + result.alphaMode = 0; + result.opacity = 1.0; + result.thinWalled = true; + return result; + } + + // Get geometry info for this BLAS (unique mesh) + GeometryInfo geoInfo = geometryInfoBuffer[blasIndex]; + + // Fetch material first so that even if geometry fetch fails we can still show a material color + // Material property fetch with bounds clamp + // Clamp material index to valid range to prevent out-of-bounds access + uint materialIndex = 0u; + if (ubo.materialCount > 0) { + materialIndex = min(geoInfo.materialIndex, (uint) (ubo.materialCount - 1)); + } + result.materialIndex = materialIndex; + + MaterialData material = materialBuffer[materialIndex]; + + // Validate buffer addresses are non-zero (geometry may still be streaming) + if (geoInfo.vertexBufferAddress == 0 || geoInfo.indexBufferAddress == 0) { + // Geometry not ready: show a stable material-derived color so the frame isn't flat gray + result.normal = float3(0, 1, 0); + float3 albedoBase = float3(material.albedo); + result.baseColor = albedoBase; + float u0 = float(materialIndex + 1); + float3 h0 = frac(float3(0.06711056, 0.00583715, 0.2065) * u0); + float3 hashColor0 = frac(h0 * 2.6180339); + result.color = saturate(0.7 * hashColor0 + 0.3 * albedoBase); + result.metallic = material.metallic; + result.roughness = material.roughness; + result.transmission = material.transmissionFactor; + result.isGlass = (material.isGlass != 0); + result.alphaMode = material.alphaMode; + result.opacity = clamp(material.alpha, 0.0, 1.0); + result.thinWalled = (material.thinWalled != 0); + return result; + } + + // Cast device addresses to typed pointers for index buffer + uint* indexBuffer = (uint*)geoInfo.indexBufferAddress; + float* vertexBuffer = (float*)geoInfo.vertexBufferAddress; + + // Validate primitive index is within range of available triangles + uint triCount = geoInfo.indexCount / 3u; + if (primitiveIndex >= triCount) { + // Out of bounds primitive; show material-derived color + result.normal = float3(0, 1, 0); + float3 albedoBase = float3(material.albedo); + result.baseColor = albedoBase; + float u1 = float(materialIndex + 1); + float3 h1 = frac(float3(0.06711056, 0.00583715, 0.2065) * u1); + float3 hashColor1 = frac(h1 * 2.6180339); + result.color = saturate(0.7 * hashColor1 + 0.3 * albedoBase); + result.metallic = material.metallic; + result.roughness = material.roughness; + result.transmission = material.transmissionFactor; + result.isGlass = (material.isGlass != 0); + result.alphaMode = material.alphaMode; + result.opacity = clamp(material.alpha, 0.0, 1.0); + result.thinWalled = (material.thinWalled != 0); + return result; + } + + // Fetch triangle indices + uint idxBase = primitiveIndex * 3u; + uint i0 = indexBuffer[idxBase + 0u]; + uint i1 = indexBuffer[idxBase + 1u]; + uint i2 = indexBuffer[idxBase + 2u]; + + if (i0 >= geoInfo.vertexCount || i1 >= geoInfo.vertexCount || i2 >= geoInfo.vertexCount) { + // Out of bounds vertex indices - still present material-derived color + result.normal = float3(0, 1, 0); + float3 albedoBase = float3(material.albedo); + result.baseColor = albedoBase; + float u2 = float(materialIndex + 1); + float3 h2 = frac(float3(0.06711056, 0.00583715, 0.2065) * u2); + float3 hashColor2 = frac(h2 * 2.6180339); + result.color = saturate(0.7 * hashColor2 + 0.3 * albedoBase); + result.metallic = material.metallic; + result.roughness = material.roughness; + result.transmission = material.transmissionFactor; + result.isGlass = (material.isGlass != 0); + result.alphaMode = material.alphaMode; + result.opacity = clamp(material.alpha, 0.0, 1.0); + result.thinWalled = (material.thinWalled != 0); + return result; + } + + // Vertex layout: pos(3) + normal(3) + texCoord(2) + tangent(4) = 12 floats per vertex + uint vertexStride = 12; // floats per vertex + + // Read object-space normals directly (offset 3 floats for position, then 3 floats for normal) + float3 n0 = float3(vertexBuffer[i0 * vertexStride + 3], + vertexBuffer[i0 * vertexStride + 4], + vertexBuffer[i0 * vertexStride + 5]); + float3 n1 = float3(vertexBuffer[i1 * vertexStride + 3], + vertexBuffer[i1 * vertexStride + 4], + vertexBuffer[i1 * vertexStride + 5]); + float3 n2 = float3(vertexBuffer[i2 * vertexStride + 3], + vertexBuffer[i2 * vertexStride + 4], + vertexBuffer[i2 * vertexStride + 5]); + + // Interpolate normal using barycentric coordinates + float3 interpolatedNormal = n0 * barycentrics.x + + n1 * barycentrics.y + + n2 * barycentrics.z; + + // Transform to world space using per-instance normal matrix + float3x3 nrmMat = float3x3(geoInfo.normalMatrix0.xyz, geoInfo.normalMatrix1.xyz, geoInfo.normalMatrix2.xyz); + float3 N = normalize(mul(nrmMat, interpolatedNormal)); + result.normal = N; + + // Read UVs and sample baseColor texture if available + float2 uv0 = float2(vertexBuffer[i0 * vertexStride + 6], vertexBuffer[i0 * vertexStride + 7]); + float2 uv1 = float2(vertexBuffer[i1 * vertexStride + 6], vertexBuffer[i1 * vertexStride + 7]); + float2 uv2 = float2(vertexBuffer[i2 * vertexStride + 6], vertexBuffer[i2 * vertexStride + 7]); + float2 uv = uv0 * barycentrics.x + uv1 * barycentrics.y + uv2 * barycentrics.z; + uv.y = 1.0 - uv.y; // flip V for glTF + result.uv = uv; + + // --- PBR texture sampling (explicit LOD 0 for compute) --- + float2 uvSample = uv; + float4 baseColor = float4(material.albedo, material.alpha); + float lodHint = computeTextureLOD(result.worldPos, material.roughness); + // For alpha-sensitive materials, prefer sampling by texture-table index even if the + // texture-set flag is missing. The texture table always provides a valid descriptor. + bool alphaSensitive = (material.alphaMode != 0) || (material.alpha < 0.999); + // For alpha-sensitive materials (decals/foliage), avoid noisy/unstable LOD selection in compute. + // Sampling baseColor at LOD 0 stabilizes alpha and prevents speckled transparency. + float baseColorLod = alphaSensitive ? 0.0 : lodHint; + if (material.baseColorTextureSet >= 0 || alphaSensitive) { + uint tiBase = (uint)min(max(material.baseColorTexIndex, 0), int(RQ_MAX_TEX - 1)); + baseColor = baseColorTex[NonUniformResourceIndex(tiBase)].SampleLevel(uvSample, baseColorLod); + baseColor *= float4(material.albedo, material.alpha); + result.texIndex = tiBase; + } else { + result.texIndex = 0u; + } + + result.baseColor = saturate(baseColor.rgb); + + float opacity = clamp(baseColor.a, 0.0, 1.0); + result.alphaMode = material.alphaMode; + result.opacity = opacity; + + // Physical descriptor: metallic-roughness (default) or spec-gloss + float4 mrOrSpecGloss = float4(1.0, 1.0, 1.0, 1.0); + if (material.physicalDescriptorTextureSet >= 0) { + uint tiPhys = (uint)min(max(material.physicalTexIndex, 0), int(RQ_MAX_TEX - 1)); + mrOrSpecGloss = baseColorTex[NonUniformResourceIndex(tiPhys)].SampleLevel(uvSample, lodHint); + } + + float metallic = 0.0; + float roughness = 1.0; + float3 F0 = float3(0.04, 0.04, 0.04); + float3 albedo = baseColor.rgb; + + if (material.useSpecGlossWorkflow != 0) { + float3 specColorSG = mrOrSpecGloss.rgb * material.specularFactor; + float gloss = clamp(mrOrSpecGloss.a * material.glossinessFactor, 0.0, 1.0); + roughness = clamp(1.0 - gloss, 0.0, 1.0); + F0 = specColorSG; + float maxF0 = max(F0.r, max(F0.g, F0.b)); + albedo = baseColor.rgb * (1.0 - maxF0); + } else { + float metallicTex = mrOrSpecGloss.b; + float roughnessTex = mrOrSpecGloss.g; + metallic = clamp(metallicTex * material.metallic, 0.0, 1.0); + roughness = clamp(roughnessTex * material.roughness, 0.0, 1.0); + F0 = lerp(float3(0.04, 0.04, 0.04), baseColor.rgb, metallic); + albedo = baseColor.rgb * (1.0 - metallic); + } + + // Ambient occlusion + float ao = material.ao; + if (material.occlusionTextureSet >= 0) { + uint tiAO = (uint)min(max(material.occlusionTexIndex, 0), int(RQ_MAX_TEX - 1)); + ao *= baseColorTex[NonUniformResourceIndex(tiAO)].SampleLevel(uvSample, lodHint).r; + } + + // Emissive + float3 emissiveTex = float3(1.0, 1.0, 1.0); + if (material.emissiveTextureSet >= 0) { + uint tiE = (uint)min(max(material.emissiveTexIndex, 0), int(RQ_MAX_TEX - 1)); + emissiveTex = baseColorTex[NonUniformResourceIndex(tiE)].SampleLevel(uvSample, lodHint).rgb; + } + float3 emissive = emissiveTex * material.emissive; + if (material.hasEmissiveStrengthExt != 0) { + emissive *= material.emissiveStrength; + } + + // Store F0 so the caller can compute Fresnel for reflection/transmission. + result.F0 = F0; + result.ior = max(material.ior, 1.0); + result.isLiquid = (material.isLiquid != 0); + + // Normal mapping (tangent space) + if (material.normalTextureSet >= 0) { + uint tiN = (uint)min(max(material.normalTexIndex, 0), int(RQ_MAX_TEX - 1)); + float3 tangentNormal = baseColorTex[NonUniformResourceIndex(tiN)].SampleLevel(uvSample, lodHint).xyz * 2.0 - 1.0; + + // Read and interpolate tangent (object-space) from vertex buffer + float4 t0 = float4(vertexBuffer[i0 * vertexStride + 8], + vertexBuffer[i0 * vertexStride + 9], + vertexBuffer[i0 * vertexStride + 10], + vertexBuffer[i0 * vertexStride + 11]); + float4 t1 = float4(vertexBuffer[i1 * vertexStride + 8], + vertexBuffer[i1 * vertexStride + 9], + vertexBuffer[i1 * vertexStride + 10], + vertexBuffer[i1 * vertexStride + 11]); + float4 t2 = float4(vertexBuffer[i2 * vertexStride + 8], + vertexBuffer[i2 * vertexStride + 9], + vertexBuffer[i2 * vertexStride + 10], + vertexBuffer[i2 * vertexStride + 11]); + float4 tan4 = t0 * barycentrics.x + t1 * barycentrics.y + t2 * barycentrics.z; + float3 T = normalize(mul(nrmMat, tan4.xyz)); + + // We flip V for glTF (uv.y = 1-uv.y). In tangent space this inverts bitangent. + // glTF tangent.w encodes bitangent sign in unflipped UV space, so negate it. + float handedness = -tan4.w; + float3 B = normalize(cross(N, T)) * handedness; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(TBN, tangentNormal)); + result.normal = N; + } + + // --- Direct lighting (GGX) --- + float3 V = normalize(-direction); + N = result.normal; + // Flip normal for backfaces so lighting works inside single-sided rooms + if (dot(N, V) < 0.0) { + N = -N; + result.normal = N; + } + + // Effective transmission for diffuse scaling (to avoid over-brightening transmissive surfaces) + float T_diff = clamp(material.transmissionFactor, 0.0, 1.0); + if (material.isGlass != 0) T_diff = max(T_diff, 0.9); + if (material.alphaMode == 2) T_diff = max(T_diff, 1.0 - clamp(baseColor.a, 0.0, 1.0)); + + float3 diffuseLighting = float3(0.0, 0.0, 0.0); + float3 specularLighting = float3(0.0, 0.0, 0.0); + + const bool shadowsEnabled = (ubo._pad1 != 0); + int lc = max(ubo.lightCount, 0); + for (int li = 0; li < lc; ++li) { + LightData light = lightBuffer[li]; + + // Determine whether to use multi-sample soft shadows for this light. + // Directional shadows stay hard for now. + int samples = 1; + float softness = max(ubo.shadowSoftness, 0.0); + int reqSamples = max(ubo.shadowSampleCount, 1); + if (shadowsEnabled && reqSamples > 1 && softness > 0.0 && light.lightType != 1) { + samples = min(reqSamples, 32); + } + + // Build a stable-ish RNG for this light + uint lightRng = rqHash(rngState ^ (uint(li) * 747796405u) ^ rqHash(asuint(result.worldPos.x) + 31u * asuint(result.worldPos.y))); + + float3 diffAcc = float3(0.0, 0.0, 0.0); + float3 specAcc = float3(0.0, 0.0, 0.0); + + for (int si = 0; si < samples; ++si) { + float3 L; + float3 radiance; + float tMaxShadow = 10000.0; + + if (light.lightType == 1) { + // Directional + L = normalize(-light.position.xyz); + radiance = light.color.rgb; + } else { + // Point/spot/emissive + float3 lightPos = light.position.xyz; + + float3 toCenter = lightPos - result.worldPos; + float dCenter = length(toCenter); + float3 Lcenter = (dCenter > 1e-5) ? (toCenter / dCenter) : float3(0, 0, 1); + + // Effective area-light radius (in meters) as a function of range. + // `shadowSoftness` is authored as a fraction of `light.range`. + float lightRadius = softness * max(light.range, 0.0); + lightRadius = clamp(lightRadius, 0.0, 2.0); + + float3 samplePos = lightPos; + if (samples > 1 && lightRadius > 0.0) { + float3 up = (abs(Lcenter.y) < 0.999) ? float3(0, 1, 0) : float3(1, 0, 0); + float3 T = normalize(cross(up, Lcenter)); + float3 B = cross(Lcenter, T); + float2 d = rqSampleDisk(lightRng); + samplePos = lightPos + (T * d.x + B * d.y) * lightRadius; + } + + float3 toLight = samplePos - result.worldPos; + float d = length(toLight); + L = (d > 1e-5) ? (toLight / d) : float3(0, 0, 1); + tMaxShadow = max(d - RQ_RAY_EPS, RQ_RAY_EPS); + + float attenuation = 1.0; + if (light.lightType == 3) { + float r = max(light.range, 0.001); + attenuation = 1.0 / (1.0 + (d / r) * (d / r)); + } else { + attenuation = 1.0 / max(d * d, 0.0001); + // GLTF style range attenuation + if (light.range > 0.0) { + attenuation *= pow(saturate(1.0 - pow(d / light.range, 4.0)), 2.0); + } + } + radiance = light.color.rgb * attenuation; + + if (light.lightType == 2) { + // Spot light cone attenuation + float3 D = normalize(light.direction.xyz); + float cd = dot(D, -L); + float cosInner = cos(light.innerConeAngle); + float cosOuter = cos(light.outerConeAngle); + float spotAttenuation = saturate((cd - cosOuter) / max(cosInner - cosOuter, 0.0001)); + spotAttenuation *= spotAttenuation; + radiance *= spotAttenuation; + } + } + + float rawDot = dot(N, L); + float NdotL = (light.lightType == 3) ? abs(rawDot) : max(rawDot, 0.0); + if (NdotL <= 0.0) { + continue; + } + + float visibility = 1.0; + if (shadowsEnabled) { + // Coplanar decals (alpha BLEND) can self-occlude against the wall behind them + // when starting shadow rays along the surface normal. Bias along the light + // direction for BLEND to keep decal lighting stable under shadows. + float3 shadowOrigin = (result.alphaMode == 2) + ? (result.worldPos + L * (4.0 * RQ_RAY_EPS)) + : (result.worldPos + N * RQ_RAY_EPS); + bool occluded = traceShadowOccluded(shadowOrigin, L, RQ_RAY_EPS, tMaxShadow); + visibility = occluded ? 0.0 : 1.0; + } + + float3 H = normalize(V + L); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + float3 spec = (D * G * F) / max(4.0 * NdotV * NdotL, 0.0001); + float3 kD = (1.0 - F) * (1.0 - metallic) * (1.0 - T_diff); + + specAcc += spec * radiance * NdotL * visibility; + diffAcc += (kD * albedo / PI) * radiance * NdotL * visibility; + } + + float inv = (samples > 1) ? (1.0 / float(samples)) : 1.0; + specularLighting += specAcc * inv; + diffuseLighting += diffAcc * inv; + } + + // Avoid ambient "fill" on true glass/transmission; it can make them look opaque/frosted. + // BUT keep ambient for alpha-BLEND decals/foliage so they don't become unnaturally dark overlays. + bool treatAsTransmissive = (material.isGlass != 0) || (material.transmissionFactor > 0.01); + float3 ambient = treatAsTransmissive ? float3(0.0, 0.0, 0.0) : (albedo * (0.1 * ubo.scaleIBLAmbient) * ao); + float3 color = ambient + diffuseLighting + specularLighting + emissive; + + result.color = color; + result.metallic = metallic; + result.roughness = roughness; + result.transmission = material.transmissionFactor; + result.isGlass = (material.isGlass != 0); + result.alphaMode = material.alphaMode; + // Keep texture-derived alpha/opacity (baseColor factor * baseColor texture). + result.opacity = clamp(baseColor.a, 0.0, 1.0); + result.thinWalled = (material.thinWalled != 0); + } + + return result; +} + +// Compute shader entry point +[[shader("compute")]] +[numthreads(8, 8, 1)] +void main(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + uint2 pixelCoord = dispatchThreadID.xy; + uint2 imageDim = uint2(ubo.screenDimensions); + + // Bounds check + if (pixelCoord.x >= imageDim.x || pixelCoord.y >= imageDim.y) { + return; + } + + // Generate primary ray + float2 uv = (float2(pixelCoord) + 0.5) / float2(imageDim); + float2 ndc = uv * 2.0 - 1.0; + + // Compute ray direction using inverse view-projection + // Unproject a point on the near plane (z=0 in Vulkan NDC) to get direction + float4x4 invProj = inverse(ubo.proj); + float4x4 invView = inverse(ubo.view); + + // Unproject near plane point (z=0) and far plane point (z=1) to get ray direction + // Near plane in clip space: z=0, w=1 + float4 nearClip = float4(ndc, 0.0, 1.0); + float4 farClip = float4(ndc, 1.0, 1.0); + + // Transform to view space + float4 nearView = mul(invProj, nearClip); + float4 farView = mul(invProj, farClip); + nearView /= nearView.w; + farView /= farView.w; + + // Transform to world space + float3 nearWorld = mul(invView, nearView).xyz; + float3 farWorld = mul(invView, farView).xyz; + + // Primary ray from camera position through the pixel + float3 rayOrigin = ubo.camPos.xyz; + float3 rayDir = normalize(farWorld - nearWorld); + + uint rngState = rqHash(pixelCoord.x + 4099u * pixelCoord.y + 131071u); + HitInfo hit = traceRay(rayOrigin, rayDir, 0.0001, 10000.0, rngState); + + if (hit.hit) { + float3 c = shadeWithSecondaryRays(rayOrigin, rayDir, hit, rngState); + // Output linear HDR-ish color; composite pass will apply exposure/gamma. + outputImage[pixelCoord] = float4(c, 1.0); + + } else { + // Sky/background color + outputImage[pixelCoord] = float4(skyColor(rayDir), 1.0); + } +} diff --git a/attachments/openxr_engine/shaders/texturedMesh.slang b/attachments/openxr_engine/shaders/texturedMesh.slang new file mode 100644 index 000000000..7c2595734 --- /dev/null +++ b/attachments/openxr_engine/shaders/texturedMesh.slang @@ -0,0 +1,113 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Combined vertex and fragment shader for textured mesh rendering +// This shader provides basic textured rendering with simple lighting + +// Input from vertex buffer +struct VSInput { + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 TexCoord; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data. Model matrix is a true 4x4, while the normal + // matrix is provided as three float4 columns that match the CPU + // layout (glm::mat3x4: 3 columns of vec4, xyz used, w unused). + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (consumes 4 locations) + [[vk::location(8)]] float4 InstanceNormal0; // normal matrix column 0 + [[vk::location(9)]] float4 InstanceNormal1; // normal matrix column 1 + [[vk::location(10)]] float4 InstanceNormal2; // normal matrix column 2 +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent to satisfy validation layer + uint ViewID : SV_ViewID; +}; + +// Uniform buffer (matched to renderer.h) +struct UniformBufferObject { + float4x4 model; + float4x4 viewProjections[4]; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D texSampler; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input, uint viewID : SV_ViewID) +{ + VSOutput output; + output.ViewID = viewID; + + // Use instance matrices directly (CPU uploads column-major model + // matrix and three float4 normal-matrix columns in attributes + // 4..10) + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + + // Transform position to world space: entity model * instance model + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); + + // Final clip space position + output.Position = mul(ubo.viewProjections[viewID], worldPos); + + // Pass world position and transformed normal to fragment shader + // (apply entity model to normals too). Reconstruct the 3x3 normal + // matrix from the three uploaded columns and apply it in column + // form to avoid any row/column layout ambiguity. + float3x3 model3x3 = (float3x3)ubo.model; + output.WorldPos = worldPos.xyz; + + float3 instNormal = + input.InstanceNormal0.xyz * input.Normal.x + + input.InstanceNormal1.xyz * input.Normal.y + + input.InstanceNormal2.xyz * input.Normal.z; + + output.Normal = normalize(mul(model3x3, instNormal)); + output.TexCoord = input.TexCoord; + output.Tangent = input.Tangent; // Pass through tangent (unused in basic rendering) + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample the texture with flipped V coordinate (glTF UV origin vs Vulkan) + float2 uv = float2(input.TexCoord.x, 1.0 - input.TexCoord.y); + float4 texColor = texSampler.Sample(uv); + + // Simple directional lighting + float3 lightDir = normalize(float3(0.5, 1.0, 0.3)); // Fixed light direction + float3 normal = normalize(input.Normal); + float lightIntensity = max(dot(normal, lightDir), 0.2); // Minimum ambient of 0.2 + + // If texture is nearly white, use a default color to avoid washed-out look + float whiteness = (texColor.r + texColor.g + texColor.b) / 3.0; + float4 finalColor = (whiteness > 0.95) + ? float4(float3(0.8, 0.8, 0.8) * lightIntensity, 1.0) + : float4(texColor.rgb * lightIntensity, texColor.a); + + return finalColor; +} diff --git a/attachments/openxr_engine/shaders/tonemapping_utils.slang b/attachments/openxr_engine/shaders/tonemapping_utils.slang new file mode 100644 index 000000000..33646b795 --- /dev/null +++ b/attachments/openxr_engine/shaders/tonemapping_utils.slang @@ -0,0 +1,34 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Tonemapping utilities for HDR to LDR conversion +// Shared between rasterization and ray query shaders + +// Hable/Uncharted 2 filmic tonemapping operator +// Provides a cinematic look with good highlight rolloff +namespace Hable_Filmic_Tonemapping { + static const float A = 0.15; + static const float B = 0.50; + static const float C = 0.10; + static const float D = 0.20; + static const float E = 0.02; + static const float F = 0.30; + static const float W = 11.2; + + float3 Uncharted2Tonemap(float3 x) { + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; + } +} diff --git a/attachments/openxr_engine/xr_context.cpp b/attachments/openxr_engine/xr_context.cpp new file mode 100644 index 000000000..821ebe486 --- /dev/null +++ b/attachments/openxr_engine/xr_context.cpp @@ -0,0 +1,694 @@ +#include "xr_context.h" +#include +#include +#include +#include +#include + +#if defined(PLATFORM_ANDROID) +#include +#include +#include +#endif + +XrContext::XrContext() {} +XrContext::~XrContext() { cleanup(); } + +bool XrContext::checkRuntimeAvailable() { + try { + std::vector extensions = xr::enumerateInstanceExtensionProperties(nullptr); + for (const auto& ext : extensions) { + if (std::strcmp(ext.extensionName, XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME) == 0) { + return true; + } + } + } catch (...) { + return false; + } + return false; +} + +bool XrContext::isExtensionEnabled(const char* extName) const { + for (const auto& ext : enabledExtensions) { + if (ext == extName) return true; + } + return false; +} + +bool XrContext::createInstance(const std::string& appName) { + std::cout << "XrContext: Creating OpenXR instance for " << appName << std::endl; + + try { + // Query available extensions + std::vector availableExtensions = xr::enumerateInstanceExtensionProperties(nullptr); + auto checkExt = [&](const char* name) { + for (const auto& ext : availableExtensions) { + if (std::strcmp(ext.extensionName, name) == 0) return true; + } + return false; + }; + + // 1. Create OpenXR Instance + std::vector extensions = { XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME }; + + // Add optional extensions if available + const char* optionalExtensions[] = { + XR_MSFT_SCENE_UNDERSTANDING_EXTENSION_NAME, + XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME, + XR_MSFT_HAND_INTERACTION_EXTENSION_NAME + }; + + for (auto ext : optionalExtensions) { + if (checkExt(ext)) { + extensions.push_back(ext); + enabledExtensions.push_back(ext); + } + } + +#if defined(PLATFORM_ANDROID) + if (checkExt(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { + extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); + enabledExtensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); + } +#endif + + xr::InstanceCreateInfo instanceCreateInfo; + std::strncpy(instanceCreateInfo.applicationInfo.applicationName, appName.c_str(), XR_MAX_APPLICATION_NAME_SIZE); + instanceCreateInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + instanceCreateInfo.enabledExtensionCount = static_cast(extensions.size()); + instanceCreateInfo.enabledExtensionNames = extensions.data(); + +#if defined(PLATFORM_ANDROID) + if (isExtensionEnabled(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { + if (!androidApp) { + std::cerr << "XrContext: androidApp not set" << std::endl; + return false; + } + + xr::InstanceCreateInfoAndroidKHR androidCreateInfo{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; + androidCreateInfo.applicationVM = androidApp->activity->vm; + androidCreateInfo.applicationActivity = androidApp->activity->clazz; + instanceCreateInfo.next = &androidCreateInfo; + } +#endif + + this->instance = xr::createInstance(instanceCreateInfo); + + // 2. Get System ID + xr::SystemGetInfo systemGetInfo; + systemGetInfo.formFactor = xr::FormFactor::HeadMountedDisplay; + this->systemId = this->instance.getSystem(systemGetInfo); + + } catch (const std::exception& e) { + std::cerr << "XrContext: Instance initialization failed: " << e.what() << std::endl; + return false; + } + + return true; +} + +bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device device, uint32_t queueFamilyIndex, uint32_t queueIndex) { + std::cout << "XrContext: Creating session" << std::endl; + + try { + // 4. Create Session + XrGraphicsBindingVulkanKHR graphicsBinding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR}; + graphicsBinding.instance = (VkInstance)this->vkInstance; + graphicsBinding.physicalDevice = (VkPhysicalDevice)physicalDevice; + graphicsBinding.device = (VkDevice)device; + graphicsBinding.queueFamilyIndex = queueFamilyIndex; + graphicsBinding.queueIndex = queueIndex; + + xr::SessionCreateInfo sessionCreateInfo; + sessionCreateInfo.next = &graphicsBinding; + sessionCreateInfo.systemId = this->systemId; + this->session = this->instance.createSession(sessionCreateInfo); + + // 5. Create Reference Space + xr::ReferenceSpaceCreateInfo spaceCreateInfo; + spaceCreateInfo.referenceSpaceType = xr::ReferenceSpaceType::Stage; + spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + this->appSpace = this->session.createReferenceSpace(spaceCreateInfo); + + // Initialize views with identity/default values to avoid out-of-bounds access + this->views.resize(2); + for (uint32_t i = 0; i < 2; ++i) { + this->views[i].pose = {{0,0,0,1}, {0,0,0}}; + this->views[i].fov = {-1, 1, 1, -1}; + } + + // 6. Initialize Action System (Chapter 7) + xr::ActionSetCreateInfo actionSetInfo; + std::strncpy(actionSetInfo.actionSetName, "main", XR_MAX_ACTION_SET_NAME_SIZE); + std::strncpy(actionSetInfo.localizedActionSetName, "Main Actions", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE); + this->actionSet = this->instance.createActionSet(actionSetInfo); + + auto createAction = [&](const std::string& name, const std::string& localizedName, xr::ActionType type) { + xr::ActionCreateInfo actionInfo; + actionInfo.actionType = type; + std::strncpy(actionInfo.actionName, name.c_str(), XR_MAX_ACTION_NAME_SIZE); + std::strncpy(actionInfo.localizedActionName, localizedName.c_str(), XR_MAX_LOCALIZED_ACTION_NAME_SIZE); + this->actions[name] = this->actionSet.createAction(actionInfo); + this->actionTypes[name] = type; + }; + + createAction("trigger_left", "Left Trigger", xr::ActionType::BooleanInput); + createAction("trigger_right", "Right Trigger", xr::ActionType::BooleanInput); + createAction("pose_left", "Left Hand Pose", xr::ActionType::PoseInput); + createAction("pose_right", "Right Hand Pose", xr::ActionType::PoseInput); + createAction("grab_left", "Left Grab", xr::ActionType::FloatInput); + createAction("grab_right", "Right Grab", xr::ActionType::FloatInput); + createAction("Grab", "Grab", xr::ActionType::BooleanInput); + createAction("GrabPose", "Grab Pose", xr::ActionType::PoseInput); + createAction("menu", "Menu Button", xr::ActionType::BooleanInput); + + // Suggest bindings for simple controller + xr::Path khrSimplePath = this->instance.stringToPath("/interaction_profiles/khr/simple_controller"); + std::vector bindings = { + {actions["trigger_left"], this->instance.stringToPath("/user/hand/left/input/select/click")}, + {actions["trigger_right"], this->instance.stringToPath("/user/hand/right/input/select/click")}, + {actions["pose_left"], this->instance.stringToPath("/user/hand/left/input/grip/pose")}, + {actions["pose_right"], this->instance.stringToPath("/user/hand/right/input/grip/pose")}, + {actions["Grab"], this->instance.stringToPath("/user/hand/right/input/select/click")}, + {actions["GrabPose"], this->instance.stringToPath("/user/hand/right/input/grip/pose")}, + {actions["menu"], this->instance.stringToPath("/user/hand/left/input/menu/click")} + }; + xr::InteractionProfileSuggestedBinding suggestedBindings; + suggestedBindings.interactionProfile = khrSimplePath; + suggestedBindings.suggestedBindings = bindings.data(); + suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); + this->instance.suggestInteractionProfileBindings(suggestedBindings); + + xr::SessionActionSetsAttachInfo attachInfo; + attachInfo.countActionSets = 1; + attachInfo.actionSets = &this->actionSet; + this->session.attachSessionActionSets(attachInfo); + + // Create spaces for pose actions + for (const auto& actionName : {"pose_left", "pose_right", "GrabPose"}) { + xr::ActionSpaceCreateInfo actionSpaceInfo; + actionSpaceInfo.action = actions[actionName]; + actionSpaceInfo.poseInActionSpace = {{0,0,0,1}, {0,0,0}}; + this->actionSpaces[actionName] = this->session.createActionSpace(actionSpaceInfo); + } + + // 7. Initialize Eye Gaze (Chapter 17) + if (isExtensionEnabled(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME)) { + xr::ReferenceSpaceCreateInfo gazeSpaceInfo; + gazeSpaceInfo.referenceSpaceType = (xr::ReferenceSpaceType)XR_REFERENCE_SPACE_TYPE_EYE_GAZE_EXT; + gazeSpaceInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + try { + this->gazeSpace = this->session.createReferenceSpace(gazeSpaceInfo); + } catch (const std::exception& e) { + std::cout << "XrContext: Eye gaze space creation failed: " << e.what() << std::endl; + } + } + + // 8. Initialize Scene Understanding (Chapter 16) + if (isExtensionEnabled(XR_MSFT_SCENE_UNDERSTANDING_EXTENSION_NAME)) { + PFN_xrCreateSceneObserverMSFT xrCreateSceneObserverMSFT_ptr; + this->instance.getInstanceProcAddr("xrCreateSceneObserverMSFT", (PFN_xrVoidFunction*)&xrCreateSceneObserverMSFT_ptr); + if (xrCreateSceneObserverMSFT_ptr) { + XrSceneObserverCreateInfoMSFT createInfo{XR_TYPE_SCENE_OBSERVER_CREATE_INFO_MSFT}; + XrSceneObserverMSFT observer; + if (xrCreateSceneObserverMSFT_ptr((XrSession)this->session, &createInfo, &observer) == XR_SUCCESS) { + this->sceneObserver = xr::SceneObserverMSFT(observer); + } + } + } + + } catch (const std::exception& e) { + std::cerr << "XrContext: Session creation failed: " << e.what() << std::endl; + return false; + } + + return true; +} + +void XrContext::cleanup() { + for (auto& swapchain : swapchains) { + swapchain.handle.destroy(); + } + swapchains.clear(); + + if (sceneObserver) { + PFN_xrDestroySceneObserverMSFT xrDestroySceneObserverMSFT_ptr; + this->instance.getInstanceProcAddr("xrDestroySceneObserverMSFT", (PFN_xrVoidFunction*)&xrDestroySceneObserverMSFT_ptr); + if (xrDestroySceneObserverMSFT_ptr) { + xrDestroySceneObserverMSFT_ptr((XrSceneObserverMSFT)sceneObserver); + } + sceneObserver = nullptr; + } + + if (gazeSpace) { + gazeSpace.destroy(); + gazeSpace = nullptr; + } + + for (auto& pair : actionSpaces) { + pair.second.destroy(); + } + actionSpaces.clear(); + + for (auto& pair : actions) { + pair.second.destroy(); + } + actions.clear(); + + if (actionSet) { + actionSet.destroy(); + actionSet = nullptr; + } + + if (appSpace) { + appSpace.destroy(); + appSpace = nullptr; + } + + if (session) { + session.destroy(); + session = nullptr; + } + + if (instance) { + instance.destroy(); + instance = nullptr; + } +} + +std::vector XrContext::getVulkanInstanceExtensions() { + if (!instance) { + // Fallback for when instance isn't created yet (though it should be) + return { "XR_KHR_vulkan_enable2" }; + } + + // Use the real API to query required extensions + std::string extensions = this->instance.getVulkanInstanceExtensionsKHR(this->systemId); + + // Parse the space-separated string into a vector of const char* + static std::vector extList; + extList.clear(); + std::string ext; + std::istringstream iss(extensions); + while (iss >> ext) { + extList.push_back(ext); + } + + static std::vector extPtrs; + extPtrs.clear(); + for (const auto& s : extList) { + extPtrs.push_back(s.c_str()); + } + + return extPtrs; +} + +std::vector XrContext::getVulkanDeviceExtensions(vk::PhysicalDevice physicalDevice) { + if (!instance) { + return { "VK_KHR_external_memory", "VK_KHR_external_semaphore" }; + } + + std::string extensions = this->instance.getVulkanDeviceExtensionsKHR(this->systemId); + + static std::vector devExtList; + devExtList.clear(); + std::string ext; + std::istringstream iss(extensions); + while (iss >> ext) { + devExtList.push_back(ext); + } + + static std::vector devExtPtrs; + devExtPtrs.clear(); + for (const auto& s : devExtList) { + devExtPtrs.push_back(s.c_str()); + } + + return devExtPtrs; +} + +const uint8_t* XrContext::getRequiredLUID() { + static uint8_t luid[8] = {0}; + xr::GraphicsRequirementsVulkanKHR graphicsRequirements; + this->instance.getVulkanGraphicsRequirements2KHR(this->systemId, &graphicsRequirements); + std::memcpy(luid, &graphicsRequirements.graphicsDeviceLuid, 8); + return luid; +} + +vk::Extent2D XrContext::getRecommendedExtent() const { + uint32_t viewCount = 0; + std::vector views = this->instance.enumerateViewConfigurationViewsToVector(this->systemId, xr::ViewConfigurationType::PrimaryStereo); + if (views.empty()) return {1024, 1024}; + return {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight}; +} + +void XrContext::createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent) { + this->format = format; + this->extent = extent; + + xr::SwapchainCreateInfo swapchainCreateInfo; + swapchainCreateInfo.arrayCount = 2; // Multiview (Layered) + swapchainCreateInfo.format = (int64_t)format; + swapchainCreateInfo.width = extent.width; + swapchainCreateInfo.height = extent.height; + swapchainCreateInfo.mipCount = 1; + swapchainCreateInfo.faceCount = 1; + swapchainCreateInfo.sampleCount = 1; + swapchainCreateInfo.usageFlags = xr::SwapchainUsageFlagBits::ColorAttachment; + + xr::Swapchain swapchainHandle = this->session.createSwapchain(swapchainCreateInfo); + + std::vector images = swapchainHandle.enumerateSwapchainImagesToVector(); + + SwapchainData data; + data.handle = swapchainHandle; + data.images = std::move(images); + this->swapchains.push_back(std::move(data)); +} + +std::vector XrContext::enumerateSwapchainImages() { + std::vector vkImages; + if (!swapchains.empty()) { + for (const auto& img : swapchains[0].images) { + vkImages.push_back((VkImage)img.image); + } + } + return vkImages; +} + +void XrContext::waitSwapchainImage() { + if (swapchains.empty()) return; + xr::SwapchainWaitInfo waitInfo; + waitInfo.timeout = xr::Duration::infinite(); + swapchains[0].handle.waitSwapchainImage(waitInfo); +} + +uint32_t XrContext::acquireSwapchainImage() { + if (swapchains.empty()) return 0; + xr::SwapchainAcquireInfo acquireInfo; + return swapchains[0].handle.acquireSwapchainImage(acquireInfo); +} + +void XrContext::releaseSwapchainImage() { + if (swapchains.empty()) return; + xr::SwapchainReleaseInfo releaseInfo; + swapchains[0].handle.releaseSwapchainImage(releaseInfo); +} + +XrFrameState XrContext::waitFrame() { + xr::FrameWaitInfo waitInfo; + this->frameState = this->session.waitFrame(waitInfo); + return (XrFrameState)this->frameState; +} + +void XrContext::beginFrame() { + xr::FrameBeginInfo beginInfo; + this->session.beginFrame(beginInfo); +} + +void XrContext::endFrame(const std::array, 2>& eyeViews) { + xr::FrameEndInfo endInfo; + endInfo.displayTime = this->frameState.predictedDisplayTime; + endInfo.environmentBlendMode = xr::EnvironmentBlendMode::Opaque; + + xr::CompositionLayerProjection layer; + layer.space = this->appSpace; + + std::vector projectionViews(2); + for (uint32_t i = 0; i < 2; ++i) { + projectionViews[i].pose = this->views[i].pose; + projectionViews[i].fov = this->views[i].fov; + projectionViews[i].subImage.swapchain = this->swapchains[0].handle; + projectionViews[i].subImage.imageRect = {{0, 0}, {(int32_t)extent.width, (int32_t)extent.height}}; + projectionViews[i].subImage.imageArrayIndex = i; + } + + layer.viewCount = (uint32_t)projectionViews.size(); + layer.views = projectionViews.data(); + + std::vector layers; + if (this->frameState.shouldRender) { + layers.push_back(reinterpret_cast(&layer)); + } + + endInfo.layerCount = (uint32_t)layers.size(); + endInfo.layers = layers.data(); + + this->session.endFrame(endInfo); +} + +void XrContext::locateViews(XrTime predictedTime) { + xr::ViewLocateInfo locateInfo; + locateInfo.viewConfigurationType = xr::ViewConfigurationType::PrimaryStereo; + locateInfo.displayTime = predictedTime; + locateInfo.space = this->appSpace; + + auto [result, viewState, locatedViews] = this->session.locateViewsToVector(locateInfo); + this->views = std::move(locatedViews); +} + +vk::Viewport XrContext::getViewport(uint32_t eye) const { + return vk::Viewport(0, 0, (float)extent.width, (float)extent.height, 0.0f, 1.0f); +} + +vk::Rect2D XrContext::getScissor(uint32_t eye) const { + return vk::Rect2D({0, 0}, extent); +} + +glm::mat4 XrContext::getProjectionMatrix(uint32_t eye) const { + if (eye >= views.size()) return glm::mat4(1.0f); + + const auto& fov = views[eye].fov; + float nearZ = 0.1f; + float farZ = 100.0f; + + float tanLeft = std::tan(fov.angleLeft); + float tanRight = std::tan(fov.angleRight); + float tanDown = std::tan(fov.angleDown); + float tanUp = std::tan(fov.angleUp); + + float tanWidth = tanRight - tanLeft; + float tanHeight = tanUp - tanDown; + + glm::mat4 projection = glm::mat4(0.0f); + projection[0][0] = 2.0f / tanWidth; + projection[1][1] = 2.0f / tanHeight; + projection[2][0] = (tanRight + tanLeft) / tanWidth; + projection[2][1] = (tanUp + tanDown) / tanHeight; + projection[2][2] = -farZ / (farZ - nearZ); + projection[2][3] = -1.0f; + projection[3][2] = -(farZ * nearZ) / (farZ - nearZ); + + return projection; +} + +glm::mat4 XrContext::getViewMatrix(uint32_t eye) const { + if (eye >= views.size()) return glm::mat4(1.0f); + return glm::inverse(xrPoseToMatrix(views[eye].pose)); +} + +glm::vec3 XrContext::getEyePosition(uint32_t eye) const { + if (eye >= views.size()) return glm::vec3(0.0f); + return glm::vec3(views[eye].pose.position.x, views[eye].pose.position.y, views[eye].pose.position.z); +} + +void XrContext::pollActions() { + if (!session || !actionSet) return; + + xr::ActionsSyncInfo syncInfo; + xr::ActiveActionSet activeSet{actionSet, xr::Space(nullptr)}; + syncInfo.activeActionSets = &activeSet; + syncInfo.countActiveActionSets = 1; + + if (session.syncActions(syncInfo) != xr::Result::Success) { + return; + } +} + +bool XrContext::isActionActive(const std::string& name) const { + if (!session || actions.find(name) == actions.end()) return false; + + xr::Action action = actions.at(name); + xr::ActionType type = actionTypes.at(name); + xr::ActionStateGetInfo getInfo{action}; + + if (type == xr::ActionType::BooleanInput) { + auto state = session.getActionStateBoolean(getInfo); + if (state.isActive) return state.currentState; + } else if (type == xr::ActionType::FloatInput) { + auto state = session.getActionStateFloat(getInfo); + if (state.isActive) return state.currentState > 0.1f; + } + + return false; +} + +XrPosef XrContext::getActionPose(const std::string& name) const { + if (!session || actionSpaces.find(name) == actionSpaces.end()) { + XrPosef pose; + pose.orientation = {0,0,0,1}; + pose.position = {0,0,0}; + return pose; + } + + xr::Space space = actionSpaces.at(name); + auto location = space.locateSpace(appSpace, frameState.predictedDisplayTime); + + if (location.locationFlags & xr::SpaceLocationFlagBits::OrientationValid && + location.locationFlags & xr::SpaceLocationFlagBits::PositionValid) { + return (XrPosef)location.pose; + } + + XrPosef pose; + pose.orientation = {0,0,0,1}; + pose.position = {0,0,0}; + return pose; +} + +std::vector XrContext::getLatestSpatialMeshes() { + if (!sceneObserver) return {}; + + std::vector meshes; + + // Use extension function pointers + PFN_xrComputeNewSceneMSFT xrComputeNewSceneMSFT_ptr; + PFN_xrGetSceneComputeStateMSFT xrGetSceneComputeStateMSFT_ptr; + PFN_xrCreateSceneMSFT xrCreateSceneMSFT_ptr; + PFN_xrGetSceneComponentsMSFT xrGetSceneComponentsMSFT_ptr; + PFN_xrGetSceneMeshBuffersMSFT xrGetSceneMeshBuffersMSFT_ptr; + PFN_xrGetSceneComponentLocationsMSFT xrGetSceneComponentLocationsMSFT_ptr; + PFN_xrDestroySceneMSFT xrDestroySceneMSFT_ptr; + + this->instance.getInstanceProcAddr("xrComputeNewSceneMSFT", (PFN_xrVoidFunction*)&xrComputeNewSceneMSFT_ptr); + this->instance.getInstanceProcAddr("xrGetSceneComputeStateMSFT", (PFN_xrVoidFunction*)&xrGetSceneComputeStateMSFT_ptr); + this->instance.getInstanceProcAddr("xrCreateSceneMSFT", (PFN_xrVoidFunction*)&xrCreateSceneMSFT_ptr); + this->instance.getInstanceProcAddr("xrGetSceneComponentsMSFT", (PFN_xrVoidFunction*)&xrGetSceneComponentsMSFT_ptr); + this->instance.getInstanceProcAddr("xrGetSceneMeshBuffersMSFT", (PFN_xrVoidFunction*)&xrGetSceneMeshBuffersMSFT_ptr); + this->instance.getInstanceProcAddr("xrGetSceneComponentLocationsMSFT", (PFN_xrVoidFunction*)&xrGetSceneComponentLocationsMSFT_ptr); + this->instance.getInstanceProcAddr("xrDestroySceneMSFT", (PFN_xrVoidFunction*)&xrDestroySceneMSFT_ptr); + + if (!xrComputeNewSceneMSFT_ptr || !xrGetSceneComputeStateMSFT_ptr || !xrCreateSceneMSFT_ptr || + !xrGetSceneComponentsMSFT_ptr || !xrGetSceneMeshBuffersMSFT_ptr || !xrGetSceneComponentLocationsMSFT_ptr || !xrDestroySceneMSFT_ptr) { + return {}; + } + + // 1. Check if we need to start a new computation or if one is ready + XrSceneComputeStateMSFT state; + xrGetSceneComputeStateMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &state); + + if (state == XR_SCENE_COMPUTE_STATE_NONE_MSFT) { + XrSceneComputeInfoMSFT computeInfo{XR_TYPE_SCENE_COMPUTE_INFO_MSFT}; + computeInfo.consistency = XR_SCENE_COMPUTE_CONSISTENCY_SNAPSHOT_MSFT; + XrSceneSphereBoundMSFT sphereBound; + sphereBound.center = {0,0,0}; + sphereBound.radius = 10.0f; + computeInfo.bounds.sphereCount = 1; + computeInfo.bounds.spheres = &sphereBound; + xrComputeNewSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &computeInfo); + return {}; + } + + if (state != XR_SCENE_COMPUTE_STATE_COMPLETED_MSFT) return {}; + + // 2. Create the scene + XrSceneCreateInfoMSFT createInfo{XR_TYPE_SCENE_CREATE_INFO_MSFT}; + XrSceneMSFT scene; + if (xrCreateSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &createInfo, &scene) != XR_SUCCESS) return {}; + + // 3. Get mesh components + XrSceneComponentsGetInfoMSFT getInfo{XR_TYPE_SCENE_COMPONENTS_GET_INFO_MSFT}; + getInfo.componentType = XR_SCENE_COMPONENT_TYPE_MESH_MSFT; + + XrSceneComponentsMSFT components{XR_TYPE_SCENE_COMPONENTS_MSFT}; + xrGetSceneComponentsMSFT_ptr(scene, &getInfo, &components); + + std::vector componentBuffer(components.componentCountOutput); + components.componentBuffer = componentBuffer.data(); + components.componentCapacityInput = components.componentCountOutput; + xrGetSceneComponentsMSFT_ptr(scene, &getInfo, &components); + + // 4. Get locations for transforms + XrSceneComponentLocationsGetInfoMSFT locGetInfo{XR_TYPE_SCENE_COMPONENT_LOCATIONS_GET_INFO_MSFT}; + locGetInfo.baseSpace = (XrSpace)appSpace; + locGetInfo.time = frameState.predictedDisplayTime; + + std::vector locationBuffer(components.componentCountOutput); + XrSceneComponentLocationsMSFT locations{XR_TYPE_SCENE_COMPONENT_LOCATIONS_MSFT}; + locations.componentCount = components.componentCountOutput; + locations.componentLocations = locationBuffer.data(); + xrGetSceneComponentLocationsMSFT_ptr(scene, &locGetInfo, &locations); + + // 5. Get mesh data for each component + for (uint32_t i = 0; i < components.componentCountOutput; ++i) { + XrSceneMeshBuffersGetInfoMSFT meshGetInfo{XR_TYPE_SCENE_MESH_BUFFERS_GET_INFO_MSFT}; + meshGetInfo.meshComponentId = componentBuffer[i].id; + + XrSceneMeshBuffersMSFT meshBuffers{XR_TYPE_SCENE_MESH_BUFFERS_MSFT}; + xrGetSceneMeshBuffersMSFT_ptr(scene, &meshGetInfo, &meshBuffers); + + XrSpatialMesh mesh; + mesh.meshGuid = componentBuffer[i].id; + mesh.vertices.resize(meshBuffers.vertexCountOutput); + mesh.indices.resize(meshBuffers.indexCountOutput); + + XrSceneMeshVertexBufferMSFT vBuffer{XR_TYPE_SCENE_MESH_VERTEX_BUFFER_MSFT}; + vBuffer.vertexCapacityInput = (uint32_t)mesh.vertices.size(); + vBuffer.vertices = (XrVector3f*)mesh.vertices.data(); + + XrSceneMeshIndexBufferMSFT iBuffer{XR_TYPE_SCENE_MESH_INDEX_BUFFER_MSFT}; + iBuffer.indexCapacityInput = (uint32_t)mesh.indices.size(); + iBuffer.indices = mesh.indices.data(); + + meshBuffers.vertexBuffer = &vBuffer; + meshBuffers.indexBuffer = &iBuffer; + + xrGetSceneMeshBuffersMSFT_ptr(scene, &meshGetInfo, &meshBuffers); + + if (locationBuffer[i].flags & (XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT)) { + mesh.transform = xrPoseToMatrix(locationBuffer[i].pose); + } else { + mesh.transform = glm::mat4(1.0f); + } + + meshes.push_back(std::move(mesh)); + } + + // 6. Cleanup scene handle + xrDestroySceneMSFT_ptr(scene); + + // Start a new computation for the next call + XrSceneComputeInfoMSFT computeInfo{XR_TYPE_SCENE_COMPUTE_INFO_MSFT}; + computeInfo.consistency = XR_SCENE_COMPUTE_CONSISTENCY_SNAPSHOT_MSFT; + XrSceneSphereBoundMSFT sphereBound; + sphereBound.center = {0,0,0}; + sphereBound.radius = 10.0f; + computeInfo.bounds.sphereCount = 1; + computeInfo.bounds.spheres = &sphereBound; + xrComputeNewSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &computeInfo); + + return meshes; +} + +glm::vec2 XrContext::getGazeNDC() const { + if (!gazeSpace || views.empty()) return glm::vec2(0.5f, 0.5f); + + auto location = gazeSpace.locateSpace(appSpace, frameState.predictedDisplayTime); + if (!(location.locationFlags & xr::SpaceLocationFlagBits::OrientationValid)) { + return glm::vec2(0.5f, 0.5f); + } + + // Project gaze vector onto the near plane + // Gaze is along the -Z axis of the gaze space + glm::mat4 gazeMat = xrPoseToMatrix((XrPosef)location.pose); + glm::vec3 gazeOrigin = glm::vec3(gazeMat[3]); + glm::vec3 gazeDir = -glm::vec3(gazeMat[2]); // Forward is -Z + + // Use the first eye's view/projection for NDC calculation + glm::mat4 viewProj = getProjectionMatrix(0) * getViewMatrix(0); + glm::vec4 projected = viewProj * glm::vec4(gazeOrigin + gazeDir, 1.0f); + + if (projected.w == 0.0f) return glm::vec2(0.5f, 0.5f); + + glm::vec3 ndc = glm::vec3(projected) / projected.w; + return glm::vec2(ndc.x * 0.5f + 0.5f, ndc.y * 0.5f + 0.5f); +} diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h new file mode 100644 index 000000000..48177e6db --- /dev/null +++ b/attachments/openxr_engine/xr_context.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Helper structure for spatial meshes (Chapter 16) +struct XrSpatialMesh { + XrGuidMSFT meshGuid; + std::vector vertices; + std::vector indices; + glm::mat4 transform; +}; + +class XrContext { +public: + XrContext(); + ~XrContext(); + + bool createInstance(const std::string& appName); + void setVulkanInstance(vk::Instance instance) { vkInstance = instance; } + bool createSession(vk::PhysicalDevice physicalDevice, vk::Device device, uint32_t queueFamilyIndex, uint32_t queueIndex); + void cleanup(); + +#if defined(PLATFORM_ANDROID) + void setAndroidApp(struct android_app* app) { androidApp = app; } +#endif + + // Core Handshake (Chapter 2) + std::vector getVulkanInstanceExtensions(); + std::vector getVulkanDeviceExtensions(vk::PhysicalDevice physicalDevice); + const uint8_t* getRequiredLUID(); + + // Swapchain Management (Chapter 3 & 8) + vk::Extent2D getRecommendedExtent() const; + void createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent); + std::vector enumerateSwapchainImages(); // Returns images with 2 layers for multiview + vk::Extent2D getSwapchainExtent() const { return extent; } + vk::Format getSwapchainFormat() const { return format; } + + void waitSwapchainImage(); + uint32_t acquireSwapchainImage(); + void releaseSwapchainImage(); + + // Frame Lifecycle (Chapter 5) + XrFrameState waitFrame(); + void beginFrame(); + void endFrame(const std::array, 2>& eyeViews); + + // View & Projection (Chapter 4 & 11) + void locateViews(XrTime predictedTime); + std::vector getLatestViews() const { return views; } + std::array getLatestViewPoses() const { return {views[0].pose, views[1].pose}; } + vk::Viewport getViewport(uint32_t eye) const; + vk::Rect2D getScissor(uint32_t eye) const; + glm::mat4 getProjectionMatrix(uint32_t eye) const; + glm::mat4 getViewMatrix(uint32_t eye) const; + glm::vec3 getEyePosition(uint32_t eye) const; + + // Input Actions (Chapter 7) + void pollActions(); + bool isActionActive(const std::string& name) const; + XrPosef getActionPose(const std::string& name) const; + + // Scene Understanding (Chapter 16) + std::vector getLatestSpatialMeshes(); + + // ML & Occlusion (Chapter 17 & 18) + glm::vec2 getGazeNDC() const; + XrReferenceSpaceType getReferenceSpace() const { return referenceSpaceType; } + + bool isExtensionEnabled(const char* extName) const; + static bool checkRuntimeAvailable(); + +private: + xr::Instance instance; + vk::Instance vkInstance; + xr::SystemId systemId; + xr::Session session; + xr::Space appSpace; + XrReferenceSpaceType referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + +#if defined(PLATFORM_ANDROID) + struct android_app* androidApp = nullptr; +#endif + + vk::Format format; + vk::Extent2D extent; + + struct SwapchainData { + xr::Swapchain handle; + std::vector images; + }; + std::vector swapchains; + + XrFrameState frameState; + std::vector views; + + // Action system members + xr::ActionSet actionSet; + std::map actions; + std::map actionTypes; + std::map actionSpaces; + + // Gaze interaction member + xr::Space gazeSpace; + + // Scene understanding member + xr::SceneObserverMSFT sceneObserver; + + std::vector enabledExtensions; +}; + +// Common Helper: Convert XrPosef to glm::mat4 +inline glm::mat4 xrPoseToMatrix(const XrPosef& pose) { + glm::quat q(pose.orientation.w, pose.orientation.x, pose.orientation.y, pose.orientation.z); + glm::mat4 m = glm::mat4_cast(q); + m[3] = glm::vec4(pose.position.x, pose.position.y, pose.position.z, 1.0f); + return m; +} diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc new file mode 100644 index 000000000..b14f11518 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc @@ -0,0 +1,26 @@ +:pp: {plus}{plus} += The OpenXR-Vulkan 1.4 Handshake: Introduction + +Before we can render a single pixel to an XR headset, we must first establish a "handshake" between our application, the OpenXR runtime, and the Vulkan graphics driver. This is not as simple as creating a standard Vulkan instance and passing it to a library; it requires a coordinated dance of extension negotiation and hardware verification. + +In this chapter, we are going to look at the three pillars of a successful spatial handshake: + +1. **System Integration**: How to extend our engine's `VulkanContext` to support the mandatory OpenXR extensions, specifically `XR_KHR_vulkan_enable2`. +2. **Hardware Alignment**: Utilizing the Locally Unique Identifier (**LUID**) to ensure that both OpenXR and Vulkan are talking to the exact same physical GPU. This is critical for cross-process memory visibility and performance. +3. **Vulkan 1.4 Requirements**: Activating the modern features—like Timeline Semaphores, Dynamic Rendering, and Synchronization 2—that allow our spatial pipeline to operate with minimal latency. + +== The Concept of the Handshake + +In a standard desktop application, your engine is the boss. It creates the instance, chooses the device, and owns the swapchain. In OpenXR, the relationship is more of a partnership. The **XR Runtime** (the software that drives the headset, like SteamVR or the Oculus service) needs to know exactly how your Vulkan instance is configured so it can safely inject its own compositor layers into your rendering stream. + +If you don't perform this handshake correctly, you might find that you can't initialize the XR session, or worse, you'll experience massive performance drops as the hardware is forced to copy images between different GPU memory contexts. + +== Vulkan 1.4 benefits in OpenXR Application + +* **Low-latency synchronization** between the CPU simulation and the GPU compositor. +* **Single-pass rendering** via multiview. +* **Direct-to-display submission** without the overhead of legacy render pass state. + +By the end of this chapter, you will have modified your engine's initialization code to be fully spatial-aware, laying the groundwork for the predictive frame loop and runtime-owned swapchains that follow. + +xref:../introduction.adoc[Previous] | xref:02_system_integration.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc new file mode 100644 index 000000000..9375dd1b8 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc @@ -0,0 +1,81 @@ +:pp: {plus}{plus} += System Integration + +In a standard desktop application, our engine is typically responsible for creating a `VkInstance` and selecting a `VkPhysicalDevice` based on its own internal logic. When we move to OpenXR, we are no longer the sole decision-maker. We must first negotiate with the XR runtime to ensure our graphics context is compatible with the headset's compositor. + +The bridge between OpenXR and Vulkan is the extension **XR_KHR_vulkan_enable2**. This extension provides the mechanism for OpenXR to specify exactly which instance and device extensions are required, as well as any specific feature flags that must be enabled for the spatial pipeline to function. + +== The Architectural Boundary: Why a Handshake? + +To understand why this negotiation is necessary, we must first understand the architectural boundary between our engine and the **OpenXR Runtime**. Unlike a standard windowing system, an XR runtime (like SteamVR, Meta Link, or Monado) often operates as a separate, high-priority system service or process. + +1. **Process Isolation**: The runtime manages the physical hardware (cameras, IMUs, and displays) and performs critical tasks like **Asynchronous Reprojection**—warping the final image if our engine misses its frame deadline. +2. **VRAM Sharing**: Because the runtime and our engine are separate processes, they must share access to the GPU's memory. This isn't just a simple copy; it is a "Zero-Copy" handoff where we both see the same physical memory addresses. +3. **Synchronization**: We need a way to tell the runtime "I am done rendering this frame" and for the runtime to tell us "I am done using this image, you can have it back." + +The handshake is our way of telling the OS and the XR runtime: "We are both speaking the same dialect of Vulkan, using the same extensions, and looking at the same physical silicon." + +== Querying XR Graphics Requirements + +Before we can even create our Vulkan instance, we must initialize our OpenXR instance and query the graphics requirements for the specific system (the headset) we are targeting. This is performed using the `xrGetVulkanGraphicsRequirements2KHR` function. + +This call populates an `XrGraphicsRequirementsVulkanKHR` structure, which contains two critical pieces of information: + +1. **minApiVersionSupported**: The minimum Vulkan version the runtime supports. +2. **maxApiVersionSupported**: The maximum Vulkan version the runtime has been tested with. + +Since we are targeting Vulkan 1.4, we must verify that our chosen version falls within this range. Most modern runtimes are rapidly updating to support the latest specifications, ensuring we have access to the advanced synchronization tools we need. + +== Extending the Engine Context + +Our engine's initialization logic needs to be modified to accept these external requirements. Instead of hardcoding a static list of instance extensions, we perform a coordinated handshake: + +1. **Query OpenXR**: Ask the runtime for its mandatory instance extensions using `xrGetVulkanInstanceExtensionsKHR`. +2. **Merge and Initialize**: Combine these OpenXR requirements with our engine's own mandatory extensions (such as `VK_EXT_debug_utils`) and create the instance. + +[IMPORTANT] +==== +**The String Parsing Pitfall**: `xrGetVulkanInstanceExtensionsKHR` returns the required extensions as a single, space-separated string (e.g., `"VK_KHR_external_memory_capabilities VK_KHR_get_physical_device_properties2"`). You must manually parse this string and split it into individual `const char*` entries before passing them to `vk::InstanceCreateInfo`. +==== + +The same principle applies to the physical device. The XR runtime may require specific device extensions, such as `VK_KHR_external_memory` or color space extensions, to safely share images between our application and the compositor process. + +[source,cpp] +---- +// Example of merging requirements in our Engine's initialization +std::vector instanceExtensions = engineDefaults.getInstanceExtensions(); + +// 1. Query the length of the extension string +uint32_t xrExtensionCount = 0; +xrGetVulkanInstanceExtensionsKHR(xrInstance, systemId, 0, &xrExtensionCount, nullptr); + +// 2. Retrieve the space-separated string +std::string xrExtensionString(xrExtensionCount, '\0'); +xrGetVulkanInstanceExtensionsKHR(xrInstance, systemId, xrExtensionCount, &xrExtensionCount, xrExtensionString.data()); + +// 3. Parse the string into individual extension names +std::stringstream ss(xrExtensionString); +std::string extension; +while (ss >> extension) { + // Check if we already have it, then add it + if (std::find(instanceExtensions.begin(), instanceExtensions.end(), extension) == instanceExtensions.end()) { + instanceExtensions.push_back(strdup(extension.c_str())); + } +} + +vk::InstanceCreateInfo createInfo({}, &applicationInfo, + static_cast(layers.size()), layers.data(), + static_cast(instanceExtensions.size()), instanceExtensions.data()); + +vk::raii::Instance instance(context, createInfo); +---- + +== Why the Handshake Matters: Zero-Copy Efficiency + +The ultimate goal of this handshake is **Zero-Copy Efficiency**. If we selected a GPU that the XR runtime couldn't talk to, the operating system would be forced to copy our final rendered frames through system memory to reach the headset's display. This would destroy our frame budget and introduce "judder"—the visual stuttering that causes motion sickness in XR. + +By following the `XR_KHR_vulkan_enable2` protocol, we guarantee that our frames stay on the GPU at all times, moving from our render pipeline to the headset's display with the absolute minimum possible latency. + +In the next section, we will look at **Hardware Alignment**, where we ensure that the `VkPhysicalDevice` we select is the exact same one the headset is physically connected to. + +xref:01_introduction.adoc[Previous] | xref:03_hardware_alignment_luid.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc new file mode 100644 index 000000000..153dca76f --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc @@ -0,0 +1,63 @@ +:pp: {plus}{plus} += Hardware Alignment (LUID) + +In many modern computing environments, especially high-end gaming desktops, it is common to have multiple GPUs—such as an integrated GPU on the processor and a dedicated high-performance card. For spatial computing, it is absolutely critical that both our application and the OpenXR runtime are using the exact same physical hardware. + +If our engine renders a frame on GPU A, but the headset is physically connected to GPU B, the operating system would be forced to copy that image across the **PCIe** (Peripheral Component Interconnect Express) bus between the two cards. This "cross-talk" introduces massive latency and can easily break the tight timing requirements needed for a comfortable XR experience. To prevent this, we use the **Locally Unique Identifier (LUID)**. + +== The "PCIe Tax": Why Alignment is Mandatory + +To understand why we care about the LUID, we must consider the cost of data movement. The PCIe bus is the highway between your CPU, system RAM, and your GPUs. While it is very fast, it is still orders of magnitude slower than the internal memory bus of a modern GPU (VRAM). + +* **Internal VRAM Copy**: Moving an image within the same GPU is almost instantaneous, often happening at speeds of 500 GB/s to 1 TB/s. +* **Cross-GPU Copy**: Moving an image from GPU A to GPU B requires sending the data over the PCIe bus to system RAM, and then back down to GPU B. This adds several milliseconds of latency. + +In XR, every millisecond counts. If your alignment is wrong, you might lose 3-5ms just moving the pixels to the display. That is nearly half of your entire frame budget for a 90Hz or 120Hz headset! + +== What is an LUID? + +An **LUID** is a 64-bit value guaranteed to be unique on the local machine until the next reboot. Unlike a **UUID** (Universally Unique Identifier), which is persistent across machines and reboots, the LUID is a transient hardware handle provided by the operating system (specifically Windows via **DXGI**, the DirectX Graphics Infrastructure, though Vulkan provides a cross-platform way to access it). + +In the context of the OpenXR-Vulkan handshake, the LUID serves as the hardware "fingerprint" of the GPU. OpenXR tells us: "I am currently talking to the GPU with this specific LUID," and we must search through our available `VkPhysicalDevice` handles until we find the one that matches. + +== Querying the XR Device + +Once we have initialized our `xr::Instance`, we can query the specific `VkPhysicalDevice` handle that the runtime expects us to use. We do this by calling `xrGetVulkanGraphicsDevice2KHR`. + +[source,cpp] +---- +VkPhysicalDevice xrRequiredDevice; +xrGetVulkanGraphicsDevice2KHR(xrInstance, systemId, *instance, &xrRequiredDevice); +---- + +While we can get the raw handle directly from the runtime, it is often safer and more robust to match based on the LUID, especially when our engine's architecture abstracts physical device selection or when multiple Vulkan instances are present. Matching via LUID ensures that we are selecting the device based on hardware identity rather than just a transient API handle. + +== Matching the LUID in Vulkan + +To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDProperties` structure. This structure contains the `deviceLUID` and a boolean `deviceLUIDValid`. In the `vulkan-hpp` RAII world, we can retrieve this by chaining structures in a `getProperties2` call. + +[source,cpp] +---- +// Iterating through physical devices to find a match +for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { + auto props2 = physicalDevice.getProperties2(); + auto idProps = props2.get(); + + if (idProps.deviceLUIDValid) { + // Compare the 8-byte LUID against the target from OpenXR + if (memcmp(idProps.deviceLUID.data(), targetLUID, 8) == 0) { + return physicalDevice; + } + } +} +---- + +== Cross-Process Memory Visibility + +The LUID isn't just for selection; it is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our game engine, the images we render must be shared across process boundaries. + +Vulkan 1.4 makes this easier by standardizing external memory handles, but these handles only work if the memory was allocated on the same physical silicon. By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies. + +In the final part of our handshake, we will ensure that our selected device is configured with the mandatory **Vulkan 1.4 features** required for a modern spatial pipeline. + +xref:02_system_integration.adoc[Previous] | xref:04_vulkan_1_4_feature_requirements.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc new file mode 100644 index 000000000..2b7f99d65 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc @@ -0,0 +1,72 @@ +:pp: {plus}{plus} += Vulkan 1.4 Feature Requirements + +The final step in our spatial handshake is ensuring that our `VkDevice` is initialized with the correct set of features. While OpenXR can technically run on older Vulkan versions, the **Vulkan 1.4** specification is the "gold standard" for modern spatial computing. It codifies several previously optional extensions into the core API, providing a guaranteed baseline of high-performance tools. + +In this section, we'll look at the three mandatory "pillars" of Vulkan 1.4 that our spatial pipeline depends on. + +== 1. Timeline Semaphores: The Async Heartbeat + +In a standard application, we use binary semaphores to sync the GPU. A binary semaphore is like a single-use "go" signal. In XR, where we have complex dependencies between the CPU simulation, the GPU render, and the XR compositor (which might be in a different process), binary semaphores are too rigid. + +**Timeline Semaphores** provide a 64-bit monotonically increasing value. Think of them as a "Global Clock" for your GPU work. This allows us to express complex "happens-before" relationships: + +* **Pacing**: We can signal that "Frame 500 is ready for composition" by simply reaching value `500` on a single timeline. +* **Late Latching**: We can tell the GPU to "Wait until value 501 is signaled by the CPU" before starting the final matrix update. +* **Cross-Process**: Because they can be exported to OS handles, timeline semaphores allow our engine and the XR runtime to stay perfectly in sync without expensive CPU read-backs. + +== 2. Synchronization 2: Mastering Queue Ownership + +The original Vulkan synchronization API was notoriously verbose and error-prone. **Synchronization 2** (`VK_KHR_synchronization2`) simplifies this by combining pipeline stages and access masks into a more unified structure (`VkDependencyInfo`). + +In XR, this is critical for handling **Queue Ownership Transfers**. Because the XR runtime owns the swapchain images, we must perform a "Release" operation on our queue when we're done rendering, and the runtime performs an "Acquire" on its side. Synchronization 2 makes these hand-offs explicit and readable, significantly reducing the risk of "Race Conditions" between our engine and the headset compositor. + +== 3. Dynamic Rendering: Ultimate Flexibility + +If you've followed our engine-building journey, you're already intimately familiar with **Dynamic Rendering**. In the context of spatial computing, this isn't just a convenient refactoring—it's an absolute necessity. + +* **Asymmetric Views**: Most headsets use "Canted Displays" where the left and right eye frustums are not parallel. This often requires different viewport and scissor settings for each eye. +* **Dynamic Resolution**: To maintain a steady 90Hz, the engine might need to drop the resolution of peripheral views instantly. +* **No Rigid State**: By using `vkCmdBeginRendering` directly on our XR swapchain images, we avoid the heavy overhead and rigid state of legacy `VkRenderPass` and `VkFramebuffer` objects. + +== Enabling Vulkan 1.4 Features in RAII + +Vulkan 1.4 simplifies feature enablement. Instead of deep, nested `pNext` chains, we can use the unified `VkPhysicalDeviceVulkan14Features` (and its predecessors for 1.3 and 1.2) to toggle the core requirements. + +[source,cpp] +---- +// Enabling mandatory features for our spatial pipeline +vk::PhysicalDeviceVulkan13Features features13; +features13.dynamicRendering = VK_TRUE; +features13.synchronization2 = VK_TRUE; + +vk::PhysicalDeviceVulkan12Features features12; +features12.timelineSemaphore = VK_TRUE; + +// Chain them together +features13.pNext = &features12; + +vk::DeviceCreateInfo createInfo({}, + static_cast(queueCreateInfos.size()), queueCreateInfos.data(), + 0, nullptr, // Layers (deprecated) + static_cast(deviceExtensions.size()), deviceExtensions.data(), + nullptr); // pEnabledFeatures (use pNext instead) + +// Ensure we are explicitly asking for Vulkan 1.4 core features if needed +// Note: In Vulkan 1.4, many of these are enabled by default if the version is supported, +// but explicit enablement is still best practice for engine portability. +createInfo.pNext = &features13; + +vk::raii::Device device(physicalDevice, createInfo); +---- + +== Summary of the Handshake + +We have now successfully: +1. Extended our `VulkanContext` to negotiate extensions with OpenXR. +2. Aligned our `VkPhysicalDevice` selection using the hardware LUID. +3. Enabled the modern Vulkan 1.4 features required for low-latency spatial rendering. + +With the handshake complete, we are ready to tackle the most significant architectural change in an XR engine: moving from engine-owned swapchains to **Runtime-Owned Swapchains**. + +xref:03_hardware_alignment_luid.adoc[Previous] | xref:05_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..7fce8783f --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc @@ -0,0 +1,179 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +In this chapter, we've explored the fundamental handshake between OpenXR and Vulkan 1.4. To bring these concepts into our `simple_game_engine`, we must evolve our core initialization logic in `renderer_core.cpp` and `xr_context.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp] and link:../../../attachments/openxr_engine/renderer_core.cpp[renderer_core.cpp]. + +== Testing for XR Mode + +Before we change the engine, we need a way to detect if we should even try to use OpenXR. In `main.cpp`, within the `main` function, we can add a check before initializing the engine: + +[source,cpp] +---- +// main.cpp +int main(int, char *[]) { + // ... initial setup ... + + Engine engine; + + // Test if we are able to run in XR mode + bool useXR = false; + if (XrContext::checkRuntimeAvailable()) { + std::cout << "OpenXR Runtime detected. Attempting spatial initialization..." << std::endl; + useXR = true; + } + + if (!engine.Initialize("Simple Engine", WINDOW_WIDTH, WINDOW_HEIGHT, ENABLE_VALIDATION_LAYERS, useXR)) { + throw std::runtime_error("Failed to initialize engine"); + } + + // ... +} +---- + +== Modifying the Renderer Initialization + +In `renderer_core.cpp`, we need to update `Renderer::Initialize` and its helper methods to handle the OpenXR handshake. + +== Supporting Code: The XrContext Helper + +Throughout this tutorial, we will use a helper class named `XrContext` (or `xrContext` as an instance) to wrap the OpenXR API. This keeps our engine code clean and focused on spatial logic. Before modifying the renderer, ensure your engine has a basic structure for this helper. + +[source,cpp] +---- +// xr_context.h +class XrContext { +public: + // Core Handshake (Chapter 2) + std::vector getVulkanInstanceExtensions(); + const uint8_t* getRequiredLUID(); + + // Swapchain Management (Chapter 3) + void createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent); + std::vector enumerateSwapchainImages(uint32_t eye); + vk::Extent2D getSwapchainExtent(); + vk::Format getSwapchainFormat(); + void waitSwapchainImage(); + uint32_t acquireSwapchainImage(uint32_t eye); + void releaseSwapchainImage(uint32_t eye); + + // Frame Lifecycle (Chapter 5) + XrFrameState waitFrame(); + void beginFrame(); + void endFrame(const std::array, 2>& views); + + // View & Projection (Chapter 4 & 11) + void locateViews(XrTime predictedTime); + std::vector getLatestViews(); + std::array getLatestViewPoses(); // Added for Late Latching + vk::Viewport getViewport(uint32_t eye); + vk::Rect2D getScissor(uint32_t eye); + glm::mat4 getProjectionMatrix(uint32_t eye); + glm::mat4 getViewMatrix(uint32_t eye); + glm::vec3 getEyePosition(uint32_t eye); // Added for UBO updates + + // Input Actions (Chapter 7) + void pollActions(); + bool isActionActive(const std::string& name); + XrPosef getActionPose(const std::string& name); + + // Scene Understanding (Chapter 16) + std::vector getLatestSpatialMeshes(); + + // ML & Occlusion (Chapter 17 & 18) + glm::vec2 getGazeNDC(); + XrReferenceSpaceType getReferenceSpace(); +}; + +// Common Helper: Convert XrPosef to glm::mat4 +inline glm::mat4 xrPoseToMatrix(const XrPosef& pose) { + glm::quat q(pose.orientation.w, pose.orientation.x, pose.orientation.y, pose.orientation.z); + glm::mat4 m = glm::mat4_cast(q); + m[3] = glm::vec4(pose.position.x, pose.position.y, pose.position.z, 1.0f); + return m; +} +---- + +== 1. Instance Creation with XR Extensions + +In `Renderer::createInstance`, we must append the extensions required by OpenXR. Instead of only relying on GLFW, we query the XR runtime: + +[source,cpp] +---- +// renderer_core.cpp +bool Renderer::createInstance(const std::string& appName, bool enableValidationLayers) { + // ... + std::vector extensions; + + // Standard GLFW extensions + #if defined(PLATFORM_DESKTOP) + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); + #endif + + // NEW: Add OpenXR mandatory extensions + if (xrMode) { + auto xrExtensions = xrContext.getVulkanInstanceExtensions(); + extensions.insert(extensions.end(), xrExtensions.begin(), xrExtensions.end()); + } + + // ... create vk::raii::Instance ... +} +---- + +=== 2. Hardware Alignment (LUID Matching) + +In `Renderer::pickPhysicalDevice`, we must ensure we select the GPU that OpenXR is actually using. We do this by matching the LUID: + +[source,cpp] +---- +// renderer_core.cpp +bool Renderer::pickPhysicalDevice() { + // ... enumerate devices ... + + vk::PhysicalDeviceIDProperties idProps; + vk::PhysicalDeviceProperties2 props2; + props2.pNext = &idProps; + + for (auto& _device : devices) { + _device.getProperties2(&props2); + + if (xrMode) { + // Match the LUID provided by OpenXR + if (std::memcmp(idProps.deviceLUID, xrContext.getRequiredLUID(), VK_LUID_SIZE) != 0) { + continue; // Not the right GPU for XR! + } + } + // ... rest of suitability scoring ... + } +} +---- + +=== 3. Enabling Vulkan 1.4 Features + +In `Renderer::createLogicalDevice`, we must explicitly enable the 1.4 features required for spatial computing: + +[source,cpp] +---- +// renderer_core.cpp +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + // ... + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + + // NEW: OpenXR requires Timeline Semaphores (often a 1.2 feature, standard in 1.4) + vk::PhysicalDeviceTimelineSemaphoreFeatures timelineFeatures; + timelineFeatures.timelineSemaphore = vk::True; + + vulkan13Features.pNext = &timelineFeatures; + + // ... create vk::raii::Device ... +} +---- + +== Why These Changes? + +By referencing the specific locations in `renderer_core.cpp`, we can see that OpenXR isn't just an "add-on"—it's a **collaborator** in the Vulkan lifecycle. Without the LUID matching in `pickPhysicalDevice`, our engine might initialize on a discrete GPU while the VR headset is tethered to an integrated one, leading to a complete failure to display. Similarly, the extension negotiation ensures that the XR compositor and our engine speak the same "dialect" of Vulkan. + +xref:04_vulkan_1_4_feature_requirements.adoc[Previous] | xref:../03_Runtime_Owned_Swapchains/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc new file mode 100644 index 000000000..5b790b8f9 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc @@ -0,0 +1,26 @@ +:pp: {plus}{plus} += Runtime-Owned Swapchains & Image Wrapping: Introduction + +In a standard Vulkan application, the engine is in total control of the swapchain. We create the `VkSwapchainKHR`, we decide the image count and format, and we own the memory. In spatial computing, this model is turned on its head. + +To achieve the near-zero latency required for a comfortable XR experience, the **XR Runtime** (the compositor) must own the final set of images. This allows the runtime to perform **Asynchronous Reprojection**—a late-stage warping of your image to match the user's head pose at the exact moment the pixels are strobed onto the display—without needing to copy your data into its own memory space. + +In this chapter, we will explore the architectural shift from engine-owned to runtime-owned resources: + +1. **External Image Negotiation**: How to request swapchain images from OpenXR and handle the formats provided by the runtime. +2. **RAII Resource Integration**: Wrapping raw `VkImage` handles provided by the runtime into our engine's `vk::raii::Image` and `vk::raii::ImageView` abstractions. +3. **Memory Ownership Lifecycle**: Mastering the **Wait-Acquire-Release** cycle, which replaces the standard `vkAcquireNextImageKHR` loop. + +== The "Wait-Acquire-Release" Rhythm + +The most significant change is how we access our render targets. In a desktop application, we ask the swapchain for an image index and render to it. In OpenXR, we must follow a strict three-step protocol: + +* **Wait**: Block until the runtime is ready to give us a swapchain image. +* **Acquire**: Formally take ownership of a specific image index. +* **Release**: Hand the image back to the compositor once our command buffer is submitted. + +This rhythm ensures that we never render into an image that is currently being displayed or warped by the compositor, preventing tearing and ensuring the highest possible visual stability. + +By the end of this chapter, your engine will be able to render its spatial views directly into the headset's compositor buffers, using our existing RAII wrappers while respecting the runtime's ownership. + +xref:../02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Previous] | xref:02_external_image_negotiation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc new file mode 100644 index 000000000..741a8dc51 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc @@ -0,0 +1,74 @@ +:pp: {plus}{plus} += External Image Negotiation + +In a standard Vulkan application, we use `vkCreateSwapchainKHR` to create a surface-bound swapchain. In OpenXR, we replace this entire mechanism with the `XrSwapchain`. Instead of the operating system's windowing system (like Win32 or X11) managing the buffers, the **XR Runtime** acts as our swapchain provider. + +This negotiation starts by asking the runtime what image formats it supports and choosing the one that best fits our engine's internal pipeline. + +== The Inversion of Control: The Swapchain Provider + +To understand this negotiation, you must shift your mindset. In a desktop app, *you* create the swapchain and the OS *consumes* it. In OpenXR, the **Runtime creates the images** and *lends* them to you. + +1. **Ownership**: The runtime allocates the VRAM and manages the memory layout. +2. **Compatibility**: The runtime knows exactly which bit-depths and color spaces its internal compositor can handle without performing expensive conversions. +3. **Efficiency**: By providing the images directly, the runtime can ensure they are allocated in a memory region that is visible to both your engine and the compositor process. + +== Enumerating Swapchain Formats + +The first step is to query the runtime for its supported `VkFormat` list using `xrEnumerateSwapchainFormats`. + +[source,cpp] +---- +uint32_t formatCount = 0; +xrEnumerateSwapchainFormats(xrSession, 0, &formatCount, nullptr); +std::vector formats(formatCount); +xrEnumerateSwapchainFormats(xrSession, formatCount, &formatCount, formats.data()); +---- + +It is important to note that OpenXR returns these as `int64_t` values, which we must cast to `VkFormat` (or `vk::Format` in our RAII engine). Usually, we want a high-dynamic-range (**HDR**) format like `VK_FORMAT_R16G16B16A16_SFLOAT` or a standard **SRGB** format like `VK_FORMAT_R8G8B8A8_SRGB`. + +[TIP] +==== +**Linear vs SRGB**: If your engine performs its own gamma correction in a post-process pass, you should prefer a linear format. However, if you are outputting directly to the headset, using an `_SRGB` format allows the hardware to perform the conversion for free during the final write. +==== + +== Creating the XrSwapchain + +Once we have chosen a format, we fill out an `XrSwapchainCreateInfo` structure. This structure is similar to `VkSwapchainCreateInfoKHR`, but it also includes XR-specific fields like **sampleCount** (for multisampling) and **faceCount** (for cubemap views). + +[source,cpp] +---- +XrSwapchainCreateInfo createInfo{XR_TYPE_SWAPCHAIN_CREATE_INFO}; +createInfo.format = chosenVulkanFormat; +createInfo.sampleCount = 1; +createInfo.width = viewWidth; +createInfo.height = viewHeight; +createInfo.faceCount = 1; +createInfo.arraySize = 1; // 1 for standard mono/stereo, more for multiview +createInfo.mipCount = 1; + +XrSwapchain xrSwapchain; +xrCreateSwapchain(xrSession, &createInfo, &xrSwapchain); +---- + +== Retrieving the Raw Images + +After creating the `XrSwapchain`, we need to get the actual `VkImage` handles so we can wrap them in our engine's abstractions. We use `xrEnumerateSwapchainImages` to retrieve an array of `XrSwapchainImageVulkanKHR` structures. + +[source,cpp] +---- +uint32_t imageCount = 0; +xrEnumerateSwapchainImages(xrSwapchain, 0, &imageCount, nullptr); +std::vector xrImages(imageCount, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); +xrEnumerateSwapchainImages(xrSwapchain, imageCount, &imageCount, (XrSwapchainImageBaseHeader*)xrImages.data()); +---- + +Each `XrSwapchainImageVulkanKHR` contains a `VkImage` handle. It is these handles that we will feed into our engine's RAII system. + +== Why "Zero-Copy" Matters: The Compositor Pipeline + +By negotiating the images this way, we ensure a **Zero-Copy** hand-off. When we finish rendering, we don't copy our frame to a separate buffer. Instead, we simply signal to the runtime that we are done with its image. The runtime then takes that exact same memory and uses it for the final compositor pass. + +This efficiency is the difference between a high-performance 90Hz experience and a laggy, uncomfortable one. In the next section, we'll see how to wrap these raw Vulkan handles into the engine's `vk::raii` abstractions. + +xref:01_introduction.adoc[Previous] | xref:03_raii_resource_integration.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc new file mode 100644 index 000000000..8b0cf4c83 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc @@ -0,0 +1,75 @@ +:pp: {plus}{plus} += RAII Resource Integration + +Once we have our raw `VkImage` handles from OpenXR, we need to integrate them into our engine's Resource Acquisition Is Initialization (**RAII**) framework. Our engine uses the `vk::raii` namespace, which provides high-level C++ wrappers that automatically manage the lifetime of Vulkan objects. + +However, there is a catch: `vk::raii` objects typically assume they own the resource and will attempt to destroy it when they go out of scope. Because the XR runtime owns these swapchain images, we must ensure that our engine does not accidentally call `vkDestroyImage` on them. + +== The Ownership Contract: "Lent, Not Given" + +When we receive a `VkImage` from `xrEnumerateSwapchainImages`, we are entering into a legal contract with the XR runtime. + +* **We can**: Bind the image as a color attachment, transition its layout, and write to it with our shaders. +* **We cannot**: Destroy the image, change its allocation flags, or attempt to free its memory. + +If our engine attempts to call `vkDestroyImage` (which a standard `vk::raii::Image` would do in its destructor), we will likely crash the entire XR system—including the headset runtime and possibly our own graphics drivers. + +== Non-Owning Image Wrappers + +The simplest way to handle this in `vulkan-hpp` is to treat the `VkImage` handles as non-owning `vk::Image` objects. While we lose the automatic cleanup of the image itself, we still gain the benefit of the type-safe C++ API. + +We then create our own `vk::raii::ImageView` for each image. Unlike the image itself, the **Image View** is something our application creates and owns, so it can (and should) be managed by our RAII system. + +[source,cpp] +---- +// Wrapping the raw handles into our engine's per-frame data +struct XrFrameBuffer { + vk::Image image; // Non-owning handle + vk::raii::ImageView imageView = nullptr; // RAII owned by us +}; + +std::vector framebuffers; +for (const auto& xrImg : xrImages) { + XrFrameBuffer fb; + fb.image = vk::Image(xrImg.image); + + // Note: If using multiview, the view type might be e2DArray instead of e2D + vk::ImageViewCreateInfo viewInfo({}, fb.image, vk::ImageViewType::e2D, + format, {}, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + + fb.imageView = vk::raii::ImageView(device, viewInfo); + framebuffers.push_back(std::move(fb)); +} +---- + +== Integrating with Dynamic Rendering + +Because we are using **Dynamic Rendering** (introduced in Vulkan 1.3 and core in 1.4), we don't need to wrap these images into `VkFramebuffer` objects. Instead, we can directly reference the `vk::raii::ImageView` in our `vk::RenderingAttachmentInfo`. + +[source,cpp] +---- +vk::RenderingAttachmentInfo colorAttachment{}; +colorAttachment.imageView = *framebuffers[imageIndex].imageView; +colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; +colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; +colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; +colorAttachment.clearValue = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + +vk::RenderingInfo renderingInfo({}, renderRect, 1, 0, 1, &colorAttachment); +commandBuffer.beginRendering(renderingInfo); +---- + +== Handling the Lifetime: Hybrid Destruction + +In our engine architecture, we keep these `XrFrameBuffer` objects alive for the duration of the `XrSwapchain`'s existence. When the user closes the application or switches environments, we destroy the `XrSwapchain` via `xrDestroySwapchain`. + +This leads to a **Hybrid Destruction** flow: +1. **Manual Destruction**: `xrDestroySwapchain` signals to the runtime that it can reclaim its VRAM. +2. **RAII Destruction**: Our `XrFrameBuffer` structures go out of scope, causing their `vk::raii::ImageView` members to automatically call `vkDestroyImageView`. +3. **Safety**: The non-owning `vk::Image` handles disappear without any Vulkan call, fulfilling our contract with the runtime. + +This hybrid approach—using raw handles for external resources and RAII for internal ones—is a common pattern when interfacing Vulkan with external runtimes like OpenXR, CUDA, or video decoders. + +In the next section, we will look at the **Memory Ownership Lifecycle**, where we master the delicate dance of waiting, acquiring, and releasing these images during our frame loop. + +xref:02_external_image_negotiation.adoc[Previous] | xref:04_memory_ownership_lifecycle.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc new file mode 100644 index 000000000..fcc79fc71 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc @@ -0,0 +1,87 @@ +:pp: {plus}{plus} += Memory Ownership Lifecycle + +In a standard desktop Vulkan application, we use `vkAcquireNextImageKHR` to get an image index from the swapchain. When we are done, we call `vkQueuePresentKHR`. In OpenXR, this is replaced by a more explicit three-step lifecycle: **Wait**, **Acquire**, and **Release**. + +This lifecycle is designed to allow the XR runtime to manage high-frequency tasks like asynchronous reprojection while keeping our engine's rendering pipeline efficient. + +== The Three-Step Dance: Syncing with the Compositor + +Unlike a desktop window, which can handle images whenever the GPU is ready, an XR headset is a rigid, time-sensitive display. The compositor needs to know exactly when it can "take" an image to show it to the user. + +1. **Wait**: "Is there an image available for me to start drawing into?" +2. **Acquire**: "Give me the index of that image and lock it for my use." +3. **Release**: "I'm done drawing; you can have it back now." + +This dance ensures that the engine and the compositor never attempt to access the same memory at the same time, preventing the "tearing" and "stuttering" that can occur if synchronization fails. + +== 1. Wait (`xrWaitSwapchainImage`) + +The first step is `xrWaitSwapchainImage`. This call is a blocking operation (with an optional timeout) that waits until the XR runtime is ready for the application to begin writing to the next image in the swapchain. + +[source,cpp] +---- +XrSwapchainImageWaitInfo waitInfo{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; +waitInfo.timeout = XR_INFINITE_DURATION; +xrWaitSwapchainImage(xrSwapchain, &waitInfo); +---- + +**Why wait?** If our engine is rendering faster than the headset's refresh rate (e.g., we are rendering at 144 FPS but the headset is 90Hz), `xrWaitSwapchainImage` will block our engine thread. This acts as the "Pacing" mechanism for our entire engine loop, ensuring we don't waste GPU power on frames that will never be shown. + +== 2. Acquire (`xrAcquireSwapchainImage`) + +Once the wait is satisfied, we call `xrAcquireSwapchainImage`. This function actually identifies which specific image index in our swapchain we should render into. + +[source,cpp] +---- +uint32_t imageIndex; +XrSwapchainImageAcquireInfo acquireInfo{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; +xrAcquireSwapchainImage(xrSwapchain, &acquireInfo, &imageIndex); +---- + +After this call, our application formally "owns" the image at `imageIndex`. This is the Vulkan-level guarantee that the compositor is no longer reading from this specific piece of VRAM. + +== 3. Synchronization and Layout Transitions + +Even though OpenXR manages the ownership, we are still responsible for Vulkan-side synchronization. When an image is returned to us, it might be in a state used by the XR compositor (usually `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` or `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`). + +We must use a **Pipeline Barrier** (ideally via **Synchronization 2** in Vulkan 1.4) to transition it to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` before we can use it in our dynamic rendering pass. + +[source,cpp] +---- +vk::ImageMemoryBarrier2 barrier{}; +barrier.oldLayout = vk::ImageLayout::eUndefined; // Safe to use undefined if we don't care about previous contents +barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; +barrier.image = framebuffers[imageIndex].image; +barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; +barrier.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; +barrier.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; +barrier.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite; + +vk::DependencyInfo depInfo({}, 0, nullptr, 0, nullptr, 1, &barrier); +commandBuffer.pipelineBarrier2(depInfo); +---- + +== 4. Release (`xrReleaseSwapchainImage`) + +Finally, once our command buffer has been submitted to the GPU (but not necessarily finished executing), we call `xrReleaseSwapchainImage`. This tells the XR runtime: "I am finished writing to this image; you may take it back for composition." + +[source,cpp] +---- +XrSwapchainImageReleaseInfo releaseInfo{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; +xrReleaseSwapchainImage(xrSwapchain, &releaseInfo); +---- + +**Crucial Detail**: Calling `Release` does not mean the GPU has finished rendering. It means the *CPU* is done recording commands. The runtime will use the synchronization primitives we enabled during the handshake (like timeline semaphores) to wait for the GPU to actually finish the work before it displays the image. + +== Summary of Resource Management + +We have now seen how to: + +1. Negotiate image formats and sizes with the XR runtime. +2. Wrap raw runtime-owned images into our RAII engine structures. +3. Manage the lifecycle of image ownership during our frame loop. + +With our resources properly managed, we can now move on to the actual rendering process. In the next chapter, we will see how to use **Dynamic Rendering** to render our spatial views directly into these images with maximum efficiency. + +xref:03_raii_resource_integration.adoc[Previous] | xref:05_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..b5d16f5b4 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc @@ -0,0 +1,73 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Transitioning from a desktop-controlled swapchain to one owned by the OpenXR runtime requires us to intercept the engine's resource creation and frame acquisition logic in `renderer_rendering.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp] and link:../../../attachments/openxr_engine/renderer_resources.cpp[renderer_resources.cpp]. + +== Adjusting Swapchain Creation + +In `renderer_rendering.cpp`, we need to modify `Renderer::createSwapChain` to bypass the standard Vulkan surface-based swapchain when in XR mode. + +[source,cpp] +---- +// renderer_rendering.cpp +bool Renderer::createSwapChain() { + if (xrMode) { + // NEW: Enumerate images provided by the OpenXR runtime for each eye + for (uint32_t eye = 0; eye < 2; eye++) { + eyeSwapchainImages[eye] = xrContext.enumerateSwapchainImages(eye); + } + + // Store extent provided by OpenXR (usually fixed per-headset) + swapChainExtent = xrContext.getSwapchainExtent(); + swapChainImageFormat = xrContext.getSwapchainFormat(); + + return createImageViews(); // Re-use existing view creation logic + } + + // ... original desktop vk::raii::SwapchainKHR logic ... +} +---- + +== Handling the Frame Lifecycle + +The "Wait-Acquire-Release" rhythm happens every frame. We must update `Renderer::Render` to synchronize with the XR compositor instead of the desktop window. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... + uint32_t imageIndex; + + if (xrMode) { + // 1. WAIT: Ensure the runtime is ready for us to start working + xrContext.waitSwapchainImage(); + + // 2. ACQUIRE: Get the index of the next available XR image for the current eye + // Note: For simplicity, we'll expand this to a per-eye loop in Chapter 4 + uint32_t eye = 0; + imageIndex = xrContext.acquireSwapchainImage(eye); + } else { + // Standard desktop acquisition + auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); + imageIndex = result.second; + } + + // ... record and submit command buffers ... + + if (xrMode) { + // 3. RELEASE: Hand the image back to the compositor for display + uint32_t eye = 0; + xrContext.releaseSwapchainImage(eye); + } else { + // Standard desktop present + presentQueue.presentKHR(presentInfo); + } +} +---- + +== Why These Changes? + +By injecting these calls into `createSwapChain` and `Render`, we maintain the engine's high-level abstractions (like `vk::raii::Image`) while relinquishing control of the **lifecycle** to the XR runtime. The `xrContext` serves as a bridge, ensuring that the engine never tries to draw into an image that the compositor is currently reading to project onto the headset's lenses. + +xref:04_memory_ownership_lifecycle.adoc[Previous] | xref:../04_Dynamic_Rendering/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc new file mode 100644 index 000000000..56cb8bceb --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc @@ -0,0 +1,16 @@ +:pp: {plus}{plus} += Dynamic Rendering for Spatial Views + +In our journey to integrate OpenXR with Vulkan 1.4, we have established a robust handshake and learned how to manage images that are actually owned by the XR runtime. However, having the images is only half the battle. We now need to render our stereo views into them. In our previous engine building series, we adopted **Dynamic Rendering** (via `VK_KHR_dynamic_rendering`) as our primary rendering path. Now, we'll see why that decision was so critical for spatial computing. + +== Why Leverage Dynamic Rendering for XR? + +For spatial computing, the dynamic rendering system we've already built offers three major advantages that a legacy render pass pipeline would struggle with: + +1. **Reduced Submission Latency**: Since we've already bypassed the need for complex render pass and framebuffer objects, we can more quickly translate our view matrices (which we'll predict in the next chapter) into command buffer submissions. +2. **Adaptive Viewports**: XR headsets often use asymmetric projections and non-parallel views. Our dynamic rendering path makes it trivial to reconfigure viewport and scissor states for each eye without needing to match them to a predefined static structure. +3. **Multiview Integration**: When we move to `VK_KHR_multiview` for single-pass stereo rendering, our existing dynamic rendering logic integrates seamlessly, allowing us to specify a `viewMask` directly in our `vk::RenderingInfo`. + +In this chapter, we will apply our engine's rendering logic to the unique requirements of spatial displays. We'll start by looking at how to bind our XR swapchain images directly to our rendering session and then move on to managing the complex viewports required by modern headsets. + +xref:../03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Previous] | xref:02_rendering_to_spatial_swapchains.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc new file mode 100644 index 000000000..5ce91b145 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc @@ -0,0 +1,62 @@ +:pp: {plus}{plus} += Rendering into Spatial Swapchains + +The beauty of the dynamic rendering system we've already built is that it doesn't care where an image comes from. Whether it's a standard swapchain or a set of images provided by an XR runtime, the process of beginning a rendering session remains consistent and lightweight. + +== Binding XR Images: The Runtime Handover + +In our previous chapter, we learned how to wrap `XrSwapchainImageVulkanKHR` handles into `vk::raii::Image` and `vk::raii::ImageView`. With dynamic rendering, we take these image views and provide them directly to the `vk::RenderingInfo` structure at the start of each frame. + +This represents a **Decoupling of the Graphics Pipeline**. Our engine no longer owns the final output buffer; it simply "paints" into whatever view the XR runtime provides. + +[source,cpp] +---- +// Define the color attachment for our XR view +vk::RenderingAttachmentInfo colorAttachment{}; +colorAttachment.imageView = *xrImageView; // Our RAII-wrapped image view from the XR runtime +colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; +colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; +colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; +colorAttachment.clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.0f, 0.0f, 0.0f, 1.0f}}}; + +// Configure the rendering session +vk::RenderingInfo renderingInfo{}; +renderingInfo.renderArea = vk::Rect2D{{0, 0}, {xrWidth, xrHeight}}; +renderingInfo.layerCount = 1; // 1 for standard mono/stereo, more if using multiview arrays +renderingInfo.colorAttachmentCount = 1; +renderingInfo.pColorAttachments = &colorAttachment; + +// Begin dynamic rendering session +commandBuffer.beginRendering(renderingInfo); + +// ... Recording draw commands as usual ... + +commandBuffer.endRendering(); +---- + +== Handling XR Pipeline State: The Power of `pNext` + +As we've seen in our engine construction, our `vk::raii::Pipeline` doesn't require a static `vk::RenderPass` object. Instead, we use `vk::PipelineRenderingCreateInfo` in the `pNext` chain of our graphics pipeline. + +For XR, this is particularly powerful because we can quickly swap between different swapchain formats (like HDR10 or standard RGBA) simply by updating this info structure during pipeline creation. This allows our engine to support **Variable Quality Levels** or different headset requirements without recompiling shaders or rebuilding complex render pass objects. + +[source,cpp] +---- +vk::PipelineRenderingCreateInfo pipelineRenderingInfo{}; +pipelineRenderingInfo.colorAttachmentCount = 1; +pipelineRenderingInfo.pColorAttachmentFormats = &xrColorFormat; +pipelineRenderingInfo.depthAttachmentFormat = vk::Format::eD32Sfloat; // If our XR view has depth + +// Link to our graphics pipeline create info +vk::GraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.pNext = &pipelineRenderingInfo; +// ... Standard pipeline configuration ... +---- + +== Decoupling the Frame Loop: Separation of Concerns + +By using dynamic rendering, we've successfully decoupled our engine's internal rendering logic from the physical output. Our `VulkanContext` remains focused on managing pipeline state and recording commands, while the OpenXR loop manages the "render area" and provides the target images. + +This **Separation of Concerns** is what allows our engine to scale seamlessly. Whether we are rendering to a 4K desktop monitor or a dual-eye VR headset, the core command recording logic remains the same. The only thing that changes is *where* the image view points and *how* the projection matrices are calculated. + +xref:01_introduction.adoc[Previous] | xref:03_stereo_viewport_scissor.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc new file mode 100644 index 000000000..0ec473568 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc @@ -0,0 +1,57 @@ +:pp: {plus}{plus} += Stereo Viewport & Scissor Management + +When rendering for XR, we aren't just drawing to a single screen. We are drawing to multiple **Views**, each with its own perspective and projection. The most common case is **Stereo** (two views), but headsets with extreme fields of view or foveal insets can require many more. To manage this in a dynamic rendering context, we must master **Viewports** and **Scissors**. + +== The Architecture of the Eye: Asymmetric Projections + +To understand why viewport and scissor management is so critical in XR, we must look at the human eye. Unlike standard 2D displays, where the viewer is typically centered and looking straight at the screen, XR lenses are often offset. + +1. **Optical Center Offset**: The center of the lens (where the image is clearest) might not be the center of the display panel. +2. **Field of View (FOV)**: Headsets often have larger outer FOVs than inner FOVs to provide a wider sense of immersion. +3. **Result**: This leads to **Asymmetric Projections**. The "frustum" is skewed. In our engine, this means we cannot simply render one eye and "flip" it for the other. Each eye must be treated as a unique camera with its own viewport and scissor constraints. + +[source,cpp] +---- +// Configure the viewport for a specific eye view +vk::Viewport eyeViewport{}; +eyeViewport.x = 0.0f; +eyeViewport.y = 0.0f; +eyeViewport.width = static_cast(viewWidth); +eyeViewport.height = static_cast(viewHeight); +eyeViewport.minDepth = 0.0f; +eyeViewport.maxDepth = 1.0f; + +// Configure the scissor to match the viewport +vk::Rect2D eyeScissor{}; +eyeScissor.offset = vk::Offset2D{0, 0}; +eyeScissor.extent = vk::Extent2D{viewWidth, viewHeight}; + +// Dynamic state update in our command buffer +commandBuffer.setViewport(0, eyeViewport); +commandBuffer.setScissor(0, eyeScissor); +---- + +== Single-Texture vs. Array-Texture Swapchains + +Depending on the XR runtime, we might receive one large "atlas" texture containing both eyes, or an **Array Texture** where each eye has its own layer. + +* **Atlasing**: We use the `x` and `y` offsets of the `vk::Viewport` and `vk::Rect2D` to draw the eyes side-by-side. +* **Array Textures**: We keep the viewport at `(0,0)` but we change the `imageView` layer in our `vk::RenderingAttachmentInfo`. + +Dynamic rendering makes it easy to handle both cases by simply adjusting the attachment info. This flexibility allows our engine to support different headset architectures (like the Pimax wide-FOV headsets vs. the Meta Quest) without rewriting the render loop. + +== Optimal View Management: The Pacing Pattern + +To minimize state transitions and optimize performance, our engine follows a specific pattern for each eye view: + +1. **Acquire the Index**: Get the swapchain index from OpenXR for the current frame. +2. **Bind the View**: Point the `vk::RenderingAttachmentInfo` to the correct eye layer or atlas region. +3. **Apply Viewport/Scissor**: Tell the GPU the exact bounds of the current eye's frustum. +4. **Issue Draw Calls**: Render the scene using the predicted view and projection matrices. + +By treating viewports and scissors as dynamic state, we avoid the heavy "Pipeline Stall" that would occur if we had to switch entire pipeline objects between eyes. This keeps our GPU utilization high and our latency low. + +With our rendering pipeline now fully adapted to the unique requirements of spatial views, we are ready to tackle the timing and pacing logic that makes XR truly immersive. In the next chapter, we will build our **Predictive Frame Loop**. + +xref:02_rendering_to_spatial_swapchains.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..e0f312725 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc @@ -0,0 +1,110 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +In this chapter, we've transitioned to direct spatial rendering by leveraging the engine's existing dynamic rendering path in `renderer_rendering.cpp`. This bypasses legacy state and allows for eye-specific viewport management. An example of this implementation can be found in link:../../../attachments/openxr_engine/renderer_rendering.cpp[renderer_rendering.cpp] and link:../../../attachments/openxr_engine/renderer_pipelines.cpp[renderer_pipelines.cpp]. + +== Direct Rendering into Spatial Swapchains + +In `renderer_rendering.cpp`, our `Render` function already uses `vk::RenderingInfo`. To support OpenXR, we must ensure we point it at the correct runtime image: + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... Wait for runtime to be ready ... + xrContext.waitSwapchainImage(); + + for (uint32_t eye = 0; eye < 2; ++eye) { + // 1. Acquire the next image for this eye + uint32_t imageIndex = xrContext.acquireSwapchainImage(eye); + + // 2. Point rendering at the acquired XR image view + // xrImageViews is our per-eye vector of swapchain image views + vk::RenderingAttachmentInfo colorAttachment{ + .imageView = *eyeSwapchainImageViews[eye][imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.1f, 0.1f, 0.1f, 1.0f}}} + }; + + vk::RenderingInfo passInfo{ + .renderArea = vk::Rect2D({0, 0}, swapChainExtent), + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment + }; + + commandBuffer.beginRendering(passInfo); + + // Issue draws for this eye + RenderEye(commandBuffer, eye); + + commandBuffer.endRendering(); + + // 3. Hand the image back to OpenXR + xrContext.releaseSwapchainImage(eye); + } +} +---- + +== Managing Stereo Viewports + +For headsets, we must render from two different viewpoints. In our engine's `Render` loop, we apply view-specific viewports and scissors: + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... inside the render loop ... + + for (uint32_t eye = 0; eye < 2; ++eye) { + // Retrieve asymmetric projection and eye-specific viewport from OpenXR + // xrContext provides these based on xrLocateViews + auto viewport = xrContext.getViewport(eye); + auto scissor = xrContext.getScissor(eye); + + commandBuffer.setViewport(0, viewport); + commandBuffer.setScissor(0, scissor); + + // Update the eye-specific View and Projection matrices in our UBO + // Note: Shaders don't need to change yet! We are rendering each eye sequentially. + updateUniformBuffer(currentFrame, eye); + + // Draw the scene for this eye + // ... + } +} +---- + +== The updateUniformBuffer Helper + +Since our engine is currently rendering eyes **sequentially** (one after another), we can reuse our existing shaders. We simply need to update the view and projection matrices in our `UniformBufferObject` for each eye pass. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::updateUniformBuffer(uint32_t currentFrame, uint32_t eye, const XrPosef* overridePose) { + // 1. Retrieve the predicted View and Projection from OpenXR + // If overridePose is null, we query the latest from the context + glm::mat4 viewMatrix = overridePose ? xrPoseToMatrix(*overridePose) : xrContext.getViewMatrix(eye); + glm::mat4 projMatrix = xrContext.getProjectionMatrix(eye); + + // 2. Update the UBO for the current frame and eye + UniformBufferObject ubo{}; + ubo.view = viewMatrix; + ubo.proj = projMatrix; + ubo.camPos = overridePose ? glm::vec3(overridePose->position.x, overridePose->position.y, overridePose->position.z) + : xrContext.getEyePosition(eye); + + // 3. Directly update the mapped memory + // uniformBuffersMapped is indexed by frame and eye + memcpy(uniformBuffersMapped[currentFrame][eye], &ubo, sizeof(UniformBufferObject)); +} +---- + +== Why These Changes? + +By binding the XR image directly into `vk::RenderingInfo` and updating viewports/scissors per-eye, we eliminate the need for an intermediate "Blit" pass. This ensures that every GPU cycle is spent on spatial content, not memory copies. At this stage, our shaders remain unchanged because we are essentially just "drawing the world twice" from different perspectives. In later chapters, we'll see how **Multiview** can collapse these two passes into one. + +xref:03_stereo_viewport_scissor.adoc[Previous] | xref:../05_Predictive_Frame_Loop/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc new file mode 100644 index 000000000..d1cd1ba8c --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc @@ -0,0 +1,26 @@ +:pp: {plus}{plus} += The Predictive Frame Loop and Timing Math + +In a traditional desktop application, the engine's main loop is often governed by simple vertical synchronization (**VSync**). We render as fast as possible, and the monitor displays the result when its scanline allows. However, in spatial computing, this "render and display" model is insufficient. If the user's head moves even slightly between the time we start rendering and the time the photons hit their retinas, the scene will appear to swim or lag, causing immediate discomfort or motion sickness. + +To solve this, OpenXR uses a **Predictive Frame Loop**. Instead of rendering the *current* state of the world, we render what the world *will* look like when the display actually strobes. + +== The Rhythm of Spatial Computing + +The OpenXR frame loop is a conversation between our engine and the XR runtime. It's not just about when to draw; it's about predicting the future. This conversation is built on three pillars: + +1. **Strict Pacing**: The runtime dictates the exact heartbeat of the application via `xrWaitFrame`. This is not just a frame limit; it's a synchronization point that aligns our engine with the display's actual refresh cycle. +2. **Predicted Display Time**: Every frame we process comes with a `predictedDisplayTime`. This is the most important number in our engine. It tells us exactly when the user will see the frame we are currently building. +3. **Simulation Alignment**: We must use this predicted time to advance our physics, animations, and—most crucially—our head tracking. We don't ask where the head *is*; we ask where the head *will be* at `predictedDisplayTime`. + +== Why Timing Math Matters + +In this chapter, we will transition our engine's main loop from its legacy `glfwWindowShouldClose` logic to the OpenXR frame lifecycle. We will learn how to: + +- Synchronize our CPU and GPU work with the XR runtime's heartbeat. +- Utilize predicted timing data to calculate accurate view and projection matrices. +- Understand the "Swim" effect and how predictive math eliminates it. + +By the end of this chapter, your engine will no longer be "reacting" to user movement; it will be "anticipating" it, providing the smooth, low-latency experience that defines high-quality spatial computing. + +xref:../04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Previous] | xref:02_xr_lifecycle.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc new file mode 100644 index 000000000..897ead42e --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc @@ -0,0 +1,71 @@ +:pp: {plus}{plus} += The XR Lifecycle + +Transitioning our engine's main loop to the OpenXR lifecycle involves three primary functions: `xrWaitFrame`, `xrBeginFrame`, and `xrEndFrame`. These functions act as the "heartbeat" of our spatial application, replacing the standard unthrottled loop or legacy `glfwPollEvents` based pacing. + +== The Temporal Paradox: Rendering the Future + +To understand the XR lifecycle, you must first accept a fundamental truth: **You are always rendering a future that hasn't happened yet.** + +In a desktop game, you render what is happening "now." In XR, because of the time it takes to record commands, submit them to the GPU, and actually light up the pixels on the display, "now" is already in the past by the time the user sees it. If we rendered the "current" head pose, the world would appear to "swim" or lag behind the user's head movements. + +The XR lifecycle is designed to solve this by providing a **Predicted Display Time**. + +== 1. `xrWaitFrame`: The Pacing Heartbeat + +The `xrWaitFrame` function is our engine's synchronization point with the XR runtime. When we call this, the runtime may block our execution until it is the optimal time for us to begin processing the next frame. + +[source,cpp] +---- +XrFrameWaitInfo frameWaitInfo{XR_TYPE_FRAME_WAIT_INFO}; +XrFrameState frameState{XR_TYPE_FRAME_STATE}; +xrWaitFrame(xrSession, &frameWaitInfo, &frameState); +---- + +* **Throttling**: If your engine is too fast, `xrWaitFrame` will block to keep you synced with the headset's refresh rate (e.g., 90Hz). +* **The Prediction**: The `frameState` object returned by this call contains the `predictedDisplayTime`. This is the exact nanosecond when the pixels we are about to render will actually be shown on the headset's display. +* **Should We Render?**: `frameState.shouldRender` tells us if the runtime actually wants a frame. If the headset is off or the user is in a system menu, we might still "wait" but skip the "render" work to save power. + +== 2. `xrBeginFrame`: Opening the Window + +Once the wait is satisfied, we call `xrBeginFrame`. This signals the start of our frame's GPU work. + +[source,cpp] +---- +XrFrameBeginInfo frameBeginInfo{XR_TYPE_FRAME_BEGIN_INFO}; +xrBeginFrame(xrSession, &frameBeginInfo); +---- + +This is our engine's cue to start recording command buffers. It is important to keep the time between `xrWaitFrame` and `xrBeginFrame` as short as possible, as this is when we perform our simulation updates and view predictions. + +== 3. `xrEndFrame`: Submitting to the Compositor + +The final step is `xrEndFrame`. This is the counterpart to `vkQueuePresentKHR`. + +[source,cpp] +---- +XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; +frameEndInfo.displayTime = frameState.predictedDisplayTime; +frameEndInfo.layerCount = 1; +frameEndInfo.pLayers = &layerPtr; +xrEndFrame(xrSession, &frameEndInfo); +---- + +Instead of just "presenting" a single image, `xrEndFrame` takes an array of **Composition Layers**. + +* **Layers**: These are separate planes of imagery (like the 3D scene, a 2D HUD, or a curved menu) that the runtime's compositor will stack together at the very last microsecond using its own high-priority GPU pass. +* **Pacing**: `xrEndFrame` tells the runtime: "My GPU work is queued. Use the synchronization we agreed upon in the handshake to know when it's safe to show this frame." + +== The Engine Loop Summary + +Our engine's main loop now follows this precise rhythm: + +1. **Poll Events**: Check for system messages (like "Headset Connected"). +2. **Wait**: Sync with the runtime heartbeat and get the future `displayTime`. +3. **Begin**: Start the frame work. +4. **Render**: Use the future time to predict poses, record Vulkan commands, and submit them. +5. **End**: Hand the finished (or queued) layers back to the runtime for display. + +By following this rhythm, we ensure our engine is perfectly synchronized with the headset's hardware, providing the smooth and stable experience required for spatial computing. + +xref:01_introduction.adoc[Previous] | xref:03_display_time_prediction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc new file mode 100644 index 000000000..90ecd4126 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc @@ -0,0 +1,56 @@ +:pp: {plus}{plus} += Display Time Prediction + +The key to a stable and immersive spatial experience is aligning our engine's internal state with the future point in time when the user will actually see the frame. This is where `predictedDisplayTime` from `XrFrameState` comes into play. We must use this timestamp for two critical tasks: **Locating the User's Head (the View)** and **Calculating our Simulation (Physics and Animation)**. + +== 1. Locating the User (`xrLocateViews`) + +When we render our views, we need to know exactly where the user's eyes will be. If we use the "current" head pose, the frame will appear to lag because the head will have moved further by the time the frame is displayed. This is known as **Motion-to-Photon Latency**. + +We use `xrLocateViews` to ask the runtime to predict the user's pose for the future `displayTime`. + +[source,cpp] +---- +XrViewLocateInfo viewLocateInfo{XR_TYPE_VIEW_LOCATE_INFO}; +viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; +viewLocateInfo.displayTime = frameState.predictedDisplayTime; +viewLocateInfo.space = xrAppSpace; + +XrViewState viewState{XR_TYPE_VIEW_STATE}; +uint32_t viewCountOutput; +std::vector views(viewCount, {XR_TYPE_VIEW}); +xrLocateViews(xrSession, &viewLocateInfo, &viewState, viewCount, &viewCountOutput, views.data()); +---- + +The resulting `XrView` array contains the predicted **Pose** (position and orientation) and **FOV** (Field of View) for each eye. Note that FOV can change! Some headsets use dynamic lenses that shift based on the user's focus, so we must update our projection matrices *every frame*. + +== 2. Simulation Alignment: Syncing Physics + +It's not just the camera that must be predicted; our entire world simulation must be aligned with `predictedDisplayTime`. This includes physics, animations, and particle systems. + +If we use a different delta time for our simulation (like a fixed 60Hz update) than for our view prediction (which is tied to the 90Hz display), the result will be a subtle but nauseating disconnect. Objects in the world will appear to "jitter" relative to the user's head movement because they are being updated on a different temporal schedule. + +In our engine, we should calculate our simulation's delta time based on the difference between the `predictedDisplayTime` of the current frame and the previous one. + +[source,cpp] +---- +double deltaTime = static_cast(currentPredictedTime - lastPredictedTime) / 1e9; // Convert nanoseconds to seconds +engine.update(deltaTime); +---- + +== The Danger of Clock Drift + +A common mistake in XR development is to use `std::chrono::now()` or `glfwGetTime()` to drive simulation. These clocks are decoupled from the XR runtime's heartbeat. + +* **System Clock**: Measures real-world wall time. +* **XR Clock**: Measures the display strobes of the headset hardware. + +Even if they start at the same time, they will eventually **Drift**. After 30 minutes of gameplay, your physics engine might be 5ms ahead or behind the display's actual refresh cycle. This drift is what causes "random" stutters and hitches that are impossible to debug if you don't use the `predictedDisplayTime` consistently. + +By using only the `predictedDisplayTime` provided by the XR runtime, we ensure that every element of our spatial scene—from the smallest particle to the user's own viewpoint—is perfectly synchronized with the hardware's display strobes. + +== Next Steps: Minimizing Latency + +Now that our frame loop is correctly paced and our simulation is aligned with the future, we have established the foundation for a high-quality spatial experience. However, there is still a small window of latency between the end of our command buffer recording and the absolute final moment before submission. In the next chapter, we will explore **Late Latching** and how **Timeline Semaphores** can help us shave off those final microseconds of "swim." + +xref:02_xr_lifecycle.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..7f6e1575d --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc @@ -0,0 +1,112 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +In this chapter, we've transitioned from a reactive desktop loop to a predictive, runtime-driven spatial heartbeat. To implement this in our `simple_game_engine`, we must modify the main loop in `engine.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/engine.cpp[engine.cpp] and link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp]. + +== Pacing the Engine Heartbeat + +In `engine.cpp`, we need to update `Engine::Run` to use the OpenXR frame lifecycle. This ensures our simulation and rendering are perfectly aligned with the headset's display. + +[source,cpp] +---- +// engine.cpp +void Engine::Run() { + // ... + while (running) { + if (renderer->IsXrMode()) { + auto& xrContext = renderer->GetXrContext(); + + // 1. WAIT: OpenXR blocks here until it's time to work on the next frame + XrFrameState frameState = xrContext.waitFrame(); + + // 2. BEGIN: Signal that we are starting CPU work for this frame + xrContext.beginFrame(); + + // Update simulation using predicted display time + Update(frameState.predictedDisplayTime); + + // 3. RENDER: Record and submit drawing commands + Render(frameState.predictedDisplayTime); + + // 4. END: Submit the finished frame to the compositor + // We pass our swapchain image views to the compositor for display + xrContext.endFrame(renderer->GetXrImageViews()); + } else { + // Standard desktop loop + deltaTimeMs = CalculateDeltaTimeMs(); + Update(deltaTimeMs); + Render(); + } + } +} +---- + +== Aligning the Camera + +In our `Update` logic, we must ensure the `activeCamera` is updated with the predicted poses from the XR runtime. + +[source,cpp] +---- +// camera_component.cpp +// Helper: Build asymmetric projection from FOV tangents +inline glm::mat4 getAsymmetricProjection(const XrFovf& fov, float nearZ, float farZ) { + float tanLeft = std::tan(fov.angleLeft); + float tanRight = std::tan(fov.angleRight); + float tanUp = std::tan(fov.angleUp); + float tanDown = std::tan(fov.angleDown); + + float tanWidth = tanRight - tanLeft; + float tanHeight = tanUp - tanDown; + + glm::mat4 projection(0.0f); + projection[0][0] = 2.0f / tanWidth; + projection[1][1] = 2.0f / tanHeight; + projection[2][0] = (tanRight + tanLeft) / tanWidth; + projection[2][1] = (tanUp + tanDown) / tanHeight; + projection[2][2] = -farZ / (farZ - nearZ); + projection[2][3] = -1.0f; + projection[3][2] = -(farZ * nearZ) / (farZ - nearZ); + + return projection; +} + +void CameraComponent::SetStereoViews(const XrView& left, const XrView& right) { + // 1. Convert OpenXR poses to 4x4 matrices + // xrPoseToMatrix is a common helper that builds a GLM matrix from a quaternion/vector + eyeViewMatrices[0] = xrPoseToMatrix(left.pose); + eyeViewMatrices[1] = xrPoseToMatrix(right.pose); + + // 2. Build asymmetric projection matrices from FOV tangents + // These are provided per-eye by the XR runtime + eyeProjectionMatrices[0] = getAsymmetricProjection(left.fov, nearZ, farZ); + eyeProjectionMatrices[1] = getAsymmetricProjection(right.fov, nearZ, farZ); +} +---- + +In `engine.cpp`, we call this within our `Update` loop: + +[source,cpp] +---- +// engine.cpp +void Engine::Update(XrTime predictedTime) { + if (renderer->IsXrMode()) { + auto& xrContext = renderer->GetXrContext(); + + // Retrieve the predicted views (poses and FOVs) from OpenXR + xrContext.locateViews(predictedTime); + auto views = xrContext.getLatestViews(); + + // Update the camera with the spatial data + if (activeCamera && views.size() >= 2) { + activeCamera->SetStereoViews(views[0], views[1]); + } + } + // ... rest of physics and entity updates ... +} +---- + +== Why These Changes? + +By moving `xrWaitFrame` into our main loop, we transform the engine from an "as-fast-as-possible" renderer into a "just-in-time" spatial engine. This eliminates the judder caused by simulation/display mismatch and ensures that when the user moves their head, the virtual world moves in perfect, predicted synchrony. + +xref:03_display_time_prediction.adoc[Previous] | xref:../06_Late_Latching/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc new file mode 100644 index 000000000..7987d516f --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc @@ -0,0 +1,18 @@ +:pp: {plus}{plus} += Late Latching and Timeline Semaphores + +In spatial computing, the gap between "I think my head is here" and "the pixel is actually drawn on the display" is known as the **Motion-to-Photon (M2P)** latency. Even with the predictive frame loop we explored in the previous chapter, there is still a window of time where the user's head can move after we've started our simulation but before the GPU has finished rendering. If we use stale head poses, the virtual world will feel like it's "swimming" or trailing behind the user's actual movement. + +This is where **Late Latching** comes in. Instead of gathering our head pose once at the start of the frame and using it for everything, we want to "latch" onto the most up-to-date pose as late as humanly possible—ideally, right before the GPU begins executing the draw calls that depend on it. + +To achieve this in Vulkan 1.4 without causing massive CPU stalls, we leverage **Timeline Semaphores**. Traditionally, synchronizing the CPU and GPU required "heavy" operations like `vkDeviceWaitIdle` or binary semaphores that are difficult to manage across complex frames. Timeline semaphores allow us to create a 64-bit monotonically increasing counter that both the CPU and GPU can wait on or signal. + +By gating our Uniform Buffer Object (UBO) or Push Constant updates behind a timeline semaphore value, we can ensure that the GPU only proceeds with a draw call once the CPU has finished writing the absolute latest view and projection matrices. This minimizes the prediction error and keeps the virtual world anchored firmly to the user's reality. + +In this chapter, we will look at how to: + +1. Orchestrate the CPU/GPU handshake using timeline semaphores. +2. Update our shader data at the "last microsecond" before submission. +3. Integrate these late-breaking updates into the engine's command buffer flow. + +xref:../05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Previous] | xref:02_last_second_update.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc new file mode 100644 index 000000000..4b1e58dc7 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc @@ -0,0 +1,52 @@ +:pp: {plus}{plus} += The Last-Second Update + +To implement late latching, we need a mechanism that allows the GPU to sit in a "ready state" for as long as possible without stalling the entire hardware pipeline. In Vulkan 1.4, the combination of **Timeline Semaphores** and **Synchronization 2** provides exactly what we need. + +== Gating the Pipeline: The Wait-Before-Submit Pattern + +Think of a timeline semaphore as a **Digital Gate**. In a standard loop, the CPU prepares everything, then sends it to the GPU. In a "Late Latching" loop, we change the order of operations: + +1. **CPU Records Commands**: We record our entire command buffer, including all the draw calls for our spatial views. However, we use a placeholder matrix or just "stale" data from the start of the frame. +2. **CPU Submits Work**: We submit the command buffer to the GPU using `vkQueueSubmit2`. +3. **GPU Waits**: Crucially, we tell the GPU to **WAIT** at the very beginning of the pipeline (usually the Vertex Shader stage) until a specific timeline semaphore reaches a target value. +4. **CPU Polls**: While the GPU is sitting at the gate, the CPU is free to keep polling OpenXR for the absolute latest head pose. +5. **The Signal**: Only at the last possible microsecond, the CPU updates the matrix in memory and **Signals** the semaphore. The GPU gate opens, and the draw calls begin with the freshest possible data. + +== Stage Mask Selection: Where to Wait? + +A critical performance decision is *where* the GPU should wait. In `vk::SemaphoreSubmitInfo`, we specify a `stageMask`. + +* **Top of Pipe**: If we wait here, the GPU does nothing. +* **Vertex Shader**: If we wait here, the GPU can still perform "Top of Pipe" work like command parsing or index fetching. It only stalls when it needs the actual vertex data. +* **Fragment Shader**: This is usually too late for late latching, as the geometry has already been rasterized based on the (potentially stale) vertex positions. + +By waiting at the **Vertex Shader** stage, we maximize hardware utilization while still ensuring that our projection and view matrices are as fresh as the hardware allows. + +[source,cpp] +---- +// Configure the timeline semaphore wait for the GPU +vk::TimelineSemaphoreSubmitInfo timelineSubmitInfo; +uint64_t waitValue = currentFrameCount + 1; +timelineSubmitInfo.setWaitSemaphoreValues(waitValue); + +vk::SubmitInfo2 submitInfo; +vk::SemaphoreSubmitInfo waitSemaphoreInfo; +waitSemaphoreInfo.setSemaphore(*lateLatchingSemaphore); +waitSemaphoreInfo.setValue(waitValue); +waitSemaphoreInfo.setStageMask(vk::PipelineStageFlagBits2::eVertexShader); + +submitInfo.setWaitSemaphoreInfos(waitSemaphoreInfo); +submitInfo.setCommandBufferInfos(commandBufferInfo); + +// The GPU will now wait at the vertex shader stage until the CPU signals waitValue +queue.submit2(submitInfo); +---- + +== Why This Kills "Swim" + +"Swim" is the nauseating effect where virtual objects seem to float or slide slightly when you move your head. It happens because of the **Latency Gap**—the time between when your engine sampled the head pose and when the pixels were actually shown. + +By using the wait-before-submit pattern, we reduce that gap from ~10-15ms down to ~1-2ms. The virtual world feels "anchored" to the physical space because the view matrices being used by the GPU were updated only microseconds before the first vertex was processed. + +xref:01_introduction.adoc[Previous] | xref:03_implementation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc new file mode 100644 index 000000000..7571b9882 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc @@ -0,0 +1,63 @@ +:pp: {plus}{plus} += Implementation + +The magic of late latching happens in the final moments before the CPU gives the GPU the "go ahead." In our engine's main loop, we've already submitted our command buffers using the `vk::SubmitInfo2` structure with the appropriate timeline semaphore wait. Now, we just need to update the data and signal the semaphore. + +== The "Last-Second" View Prediction + +To get the absolute latest head pose, we call `xrLocateSpace` using the `predictedDisplayTime` we calculated in the previous chapter. + +[source,cpp] +---- +// 1. Get the absolute latest head pose from OpenXR +XrSpaceLocation spaceLocation{XR_TYPE_SPACE_LOCATION}; +XrResult result = xrLocateSpace(viewSpace, baseSpace, predictedDisplayTime, &spaceLocation); + +if (XR_UNQUALIFIED_SUCCESS(result) && (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) { + // 2. Convert XrSpaceLocation to our engine's internal View/Projection matrices + auto latestViewMatrix = convertXrPoseToMatrix(spaceLocation.pose); + + // 3. Directly update the Host-Visible UBO buffer (already persistently mapped) + std::memcpy(mappedUboPointer, &latestViewMatrix, sizeof(latestViewMatrix)); + + // 4. Flush the memory range if it's not coherent + // This ensures the GPU can see our update immediately + vk::MappedMemoryRange flushRange; + flushRange.setMemory(*uboBufferMemory); + flushRange.setOffset(0); + flushRange.setSize(sizeof(latestViewMatrix)); + device.flushMappedMemoryRanges(flushRange); +} + +// 5. Signal the timeline semaphore to value X, unblocking the GPU +vk::SemaphoreSignalInfo signalInfo; +signalInfo.setSemaphore(*lateLatchingSemaphore); +signalInfo.setValue(waitValue); +device.signalSemaphore(signalInfo); +---- + +== Persistent Mapping: The Efficiency Requirement + +Wait, there’s an important architectural detail here: **Persistent Mapping**. For late latching to work effectively, we cannot afford the overhead of calling `vkMapMemory` and `vkUnmapMemory` every frame. + +* **The Concept**: We map the Vulkan buffer once during initialization and keep the CPU-side pointer (`mappedUboPointer`) available for the lifetime of the application. +* **The Why**: Mapping memory is an expensive system call that often involves kernel-mode transitions. In our late latching loop, we have less than a millisecond to perform the update. We need the raw performance of a direct `memcpy`. + +== The Coherency Question: `vkFlushMappedMemoryRanges` + +Not all GPU memory is "Coherent." On some hardware (especially discrete desktop GPUs), the CPU and GPU have separate caches. Even if we `memcpy` the data, the GPU might still see the "stale" version in its cache. + +If our memory heap doesn't have the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` flag, we must call `vkFlushMappedMemoryRanges`. This forces the CPU cache to write the data out to physical VRAM where the GPU can see it. In our late latching implementation, this flush is the final step before we open the digital gate. + +== Limitations of Late Latching + +This technique is most effective for "fast-moving" data like the head pose or hand tracking data. However, it’s not suitable for updating entire scene hierarchies or large amounts of vertex data. + +* **Data Size**: Keep your late-latched updates small (usually just a few matrices). +* **Timing**: If the update takes too long, the GPU will idle while waiting for the semaphore, which can reduce your overall framerate. + +By focusing purely on the view and projection matrices, we keep the overhead minimal and the visual latency as low as the hardware allows. + +With late latching implemented, our virtual environment is now as anchored as possible. In the next chapter, we will shift our focus to **Action Spaces**—how we turn user input (button clicks and hand movements) into meaningful interactions within this spatially-aligned world. + +xref:02_last_second_update.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..87a23b534 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc @@ -0,0 +1,60 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Late Latching is a powerful optimization that minimizes "swim" by updating view and projection matrices at the last possible moment before GPU submission. To implement this in our `simple_game_engine`, we modify the `Render` loop in `renderer_rendering.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/renderer_rendering.cpp[renderer_rendering.cpp]. + +== Implementing the Late Update + +In `renderer_rendering.cpp`, we need to delay the final UBO update until just before the `vkCmdDraw` calls. This involves using **Timeline Semaphores** to gate our command buffer submission until the last-second matrices are ready. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... + if (xrMode) { + // Retrieve the ABSOLUTE LATEST view poses from OpenXR + // Note: This happens AFTER we've already waited on the frame fence + auto latestPoses = xrContext.getLatestViewPoses(); + + // Directly update the mapped UBO memory for each eye + // Since we use persistent mapping (vk::MemoryPropertyFlagBits::eHostVisible), + // we can write directly to the GPU-resident buffers. + for (uint32_t eye = 0; eye < 2; ++eye) { + updateUniformBuffer(currentFrame, eye, &latestPoses[eye]); + } + + // Use Timeline Semaphores to signal that the late update is finished + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = ¤tFrameValue + }; + + // Gate the graphics queue submission + // The GPU will wait for 'currentFrameValue' before executing + graphicsQueue.submit(submitInfo); + } +} +---- + +== Persistent Mapping for Low Latency + +In `Renderer::createUniformBuffers`, we ensure our buffers are mapped once and held open for the lifetime of the renderer, avoiding the overhead of per-frame mapping. + +[source,cpp] +---- +// renderer_rendering.cpp +bool Renderer::createUniformBuffers(Entity* entity) { + // ... create vk::raii::Buffer ... + + // NEW: Persistently map the buffer for last-second updates + // entityRes.uniformBuffersMapped is a void* array for each frame-in-flight + entityRes.uniformBuffersMapped[i] = entityRes.uniformBuffersAllocation[i]->map(0, VK_WHOLE_SIZE); +} +---- + +== Why These Changes? + +By moving the `updateUniformBuffer` call to the very end of the `Render` function (just before submission), we ensure that the GPU receives the most accurate head-tracking data possible. This reduces the "perceived latency" (Motion-to-Photon) from the time we recorded the frame to the time it's actually scanned out to the headset's display. + +xref:03_implementation.adoc[Previous] | xref:../07_Action_Spaces_Input/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc new file mode 100644 index 000000000..054d69fcd --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc @@ -0,0 +1,17 @@ +:pp: {plus}{plus} += Action Spaces and Input-to-Pipeline Flow + +In a traditional PC or mobile game, we often think about input in terms of raw physical devices: "Was the A button pressed?" or "What is the mouse delta?" However, in spatial computing, this model breaks down. The diversity of VR and AR controllers—ranging from standard gamepads and 6DoF hand controllers to hand-tracking and eye-tracking—means that our engine should never care about the *specific* button a user is pressing. + +Instead, OpenXR uses an **Action-Based Input System**. This abstraction layer allows us to define what we want the user to *do* (like "Grab", "Teleport", or "Menu") and then map those actions to various hardware profiles later. This ensures that our engine is future-proof and compatible with all current and future devices. + +But in spatial computing, input isn't just about buttons; it's about **Pose Actions**. When a user moves their hand, we don't just get a button event; we get an entire spatial transformation—a position and an orientation in 3D space. These actions are tracked within **Spaces**, which we've briefly touched on before. + +In this chapter, we will explore: +1. How to define and map OpenXR Actions to physical controller profiles. +2. The concept of **Action Spaces**, which bridge the gap between input and the rendering pipeline. +3. How to convert raw XrSpace poses into 4x4 transform matrices that our Vulkan shaders can use for interaction. + +By the end of this section, we will have a robust system for handling both digital input (button clicks) and spatial input (hand positions) in a way that is clean, modular, and performant. + +xref:../06_Late_Latching/04_incorporating_into_the_engine.adoc[Previous] | xref:02_openxr_action_system.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc new file mode 100644 index 000000000..c17749d40 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc @@ -0,0 +1,80 @@ +:pp: {plus}{plus} += The OpenXR Action System + +The OpenXR input system is built around three core concepts: **Action Sets**, **Actions**, and **Interaction Profiles**. By separating our engine's logic from the physical hardware, we ensure that our application remains robust and adaptable across a vast ecosystem of controllers. + +== The Hardware Abstraction Layer: Thinking in Actions + +In a legacy input system, you might write code like `if (buttonA.isPressed()) { jump(); }`. This is a fragile approach in XR, where one user might have an Oculus Touch controller (with an 'A' button) and another might have Valve Index controllers (with a pressure-sensitive trackpad). + +OpenXR uses an **Action-Based** model. Instead of looking for buttons, your engine defines what the user can *do*: + +1. **Define the Action**: "I need an action called 'Jump'." +2. **Assign the Type**: Is it a boolean (on/off), a float (analog trigger), or a pose (hand position)? +3. **Suggest the Binding**: "On an Oculus controller, map 'Jump' to the 'A' button. On a Vive controller, map it to the 'Trackpad Click'." + +The XR runtime handles the "Translation" at the last possible second, giving your engine a clean, unified stream of "Jump" events regardless of the physical hardware. + +== 1. Action Sets: Contextual Input + +An **Action Set** is a container for related actions. This allows you to enable or disable groups of inputs based on what the user is doing. + +* **Gameplay Set**: Contains "Teleport", "Grab", and "Shoot". +* **Menu Set**: Contains "Select" and "Back". + +[source,cpp] +---- +// 1. Create an Action Set +XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; +std::strcpy(actionSetInfo.actionSetName, "gameplay"); +std::strcpy(actionSetInfo.localizedActionSetName, "Main Gameplay"); +actionSetInfo.priority = 0; + +XrActionSet gameplayActionSet; +xrCreateActionSet(instance, &actionSetInfo, &gameplayActionSet); +---- + +[TIP] +==== +**Localized Names**: The `localizedActionSetName` is important! The XR runtime may show this to the user in a "Controller Binding" menu, allowing them to remap your actions to their own preferences. +==== + +== 2. Actions: Defining Intent + +Within an action set, we define our individual **Actions**. + +[source,cpp] +---- +// 2. Create a Pose Action for hand tracking +XrActionCreateInfo actionInfo{XR_TYPE_ACTION_CREATE_INFO}; +actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT; +std::strcpy(actionInfo.actionName, "hand_pose"); +std::strcpy(actionInfo.localizedActionName, "Hand Pose"); +actionInfo.countSubactionPaths = 0; + +XrAction handPoseAction; +xrCreateAction(gameplayActionSet, &actionInfo, &handPoseAction); +---- + +== 3. Interaction Profiles: Suggesting the Map + +Finally, we suggest how these actions should map to real hardware. We don't "force" a mapping; we provide a "Suggested Binding" for each known controller type. + +[source,cpp] +---- +// 3. Map the action to a specific controller path +std::vector bindings; +// We use standardized OpenXR paths like /user/hand/right/input/grip/pose +bindings.push_back({handPoseAction, xrPathToString(instance, "/user/hand/right/input/grip/pose")}); + +XrInteractionProfileSuggestedBinding profileBinding{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; +profileBinding.interactionProfile = xrPathToString(instance, "/interaction_profiles/oculus/touch_controller"); +profileBinding.suggestedBindings = bindings.data(); +profileBinding.countSuggestedBindings = bindings.size(); + +xrSuggestInteractionProfileBindings(instance, &profileBinding); +---- + +This abstraction is especially critical for **Space Manifolds**, where a user's physical movement is converted into a 3D coordinate system for our renderer—the topic of our next section. + +xref:01_introduction.adoc[Previous] | xref:03_space_manifolds.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc new file mode 100644 index 000000000..2d38f20f0 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc @@ -0,0 +1,55 @@ +:pp: {plus}{plus} += Space Manifolds + +While most input actions result in discrete events (like a button press), pose actions are special: they are inherently spatial. To bridge the gap between the user's hand movement and the objects in our 3D engine, we need to create an **Action Space**. + +== Virtual Coordinate Systems: The Action Space + +Think of an action space as a virtual coordinate system that "follows" the user's hand. In OpenXR, all spatial tracking is relative. To find where a hand is, we must ask: "Where is this Hand Space relative to my World Space?" + +1. **Grip Pose**: The location of the user's palm. This is used for holding objects like a sword or a steering wheel. +2. **Aim Pose**: The location and orientation of a ray extending from the controller's front. This is used for pointing, laser-pointers, and UI interaction. + +[source,cpp] +---- +// 1. Create a Space for our Hand Pose Action +XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; +actionSpaceInfo.action = handPoseAction; +// This allows us to apply an offset (e.g., to align the virtual hand with the physical controller) +actionSpaceInfo.poseInActionSpace = identityPose; + +XrSpace handSpace; +xrCreateActionSpace(session, &actionSpaceInfo, &handSpace); +---- + +== Space Manifolds: From Quaternions to Matrices + +Every frame, we use `xrLocateSpace` to find where this hand space is located. The runtime provides this as an `XrSpaceLocation`, which contains a position (vector) and an orientation (quaternion). + +However, our Vulkan shaders typically expect a **4x4 Matrix** for spatial transformations. This conversion is the "Space Manifold"—mapping the 7D tracking data (3 position + 4 quaternion) into the 16D matrix format used by the GPU's fixed-function vertex hardware. + +[source,cpp] +---- +// 2. Locate the hand space relative to the world +XrSpaceLocation location{XR_TYPE_SPACE_LOCATION}; +xrLocateSpace(handSpace, worldSpace, predictedDisplayTime, &location); + +if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { + // 3. Convert XrPosef to a 4x4 transformation matrix + // This matrix represents the manifold of the controller's path through the virtual world + vk::mat4 handTransform = xrPoseToMatrix(location.pose); + + // Pass this matrix to our engine's scene graph or directly to a shader + pushConstants.modelMatrix = handTransform; +} +---- + +== Why We Use "App Space" + +In spatial computing, the **App Space** (or Stage Space) is the origin of our virtual world. By locating the `handSpace` relative to the `worldSpace`, we ensure that if the user walks around their room, the virtual hand stays perfectly aligned with their physical hand relative to the virtual environment. + +This transformation process effectively turns a physical controller into a **Space Manifold**—a virtual object that moves through our world's coordinate system. Whether the user is pointing a laser, picking up a sword, or simply waving their hand, the engine sees it all as a single, consistent matrix. + +In the next chapter, we will look at how to optimize the rendering of these multiple views using **Slang for Spatial Shaders**. + +xref:02_openxr_action_system.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..6a2a87d6b --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc @@ -0,0 +1,46 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +OpenXR's Action System requires us to move away from polling raw buttons (like `glfwGetMouseButton`) and toward an abstract, intent-based input system in `engine.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp] and link:../../../attachments/openxr_engine/engine.cpp[engine.cpp]. + +== Mapping Actions in the Engine + +In `engine.cpp`, we update our `Update` logic to process OpenXR actions. Instead of checking for a specific mouse button to "ThrowBall", we check for a "Grab" action. + +[source,cpp] +---- +// engine.cpp +void Engine::Update(XrTime predictedTime) { + if (renderer->IsXrMode()) { + auto& xrContext = renderer->GetXrContext(); + + // 1. Poll OpenXR action states (Grab, Teleport, etc.) + xrContext.pollActions(); + + if (xrContext.isActionActive("Grab")) { + // 2. Retrieve the spatial pose of the controller (Action Space) + XrPosef handPose = xrContext.getActionPose("GrabPose"); + + // 3. Convert the XR pose into our engine's 4x4 matrix + glm::mat4 handMatrix = xrPoseToMatrix(handPose); + + // Spawn the ball at the controller's exact spatial location + ThrowBallFromHand(handMatrix); + } + } else { + // Standard desktop mouse input + if (platform->GetMouseButton(1)) { + ThrowBall(mouseX, mouseY); + } + } + + // Standard physics/entity update + Update(deltaTimeMs); +} +---- + +== Why These Changes? + +By decoupling input from physical hardware (like a specific controller's trigger), we make our engine cross-platform out of the box. Whether the user is using an Oculus Touch controller or a Valve Index "knuckle," the engine only sees the "Grab" action and the associated spatial pose, allowing us to focus on the physics of the interaction rather than the nuances of different hardware drivers. + +xref:03_space_manifolds.adoc[Previous] | xref:../08_Slang_Spatial_Shaders/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc new file mode 100644 index 000000000..fbe350ba0 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc @@ -0,0 +1,23 @@ +:pp: {plus}{plus} += Slang for Spatial Shaders: Single-Pass Stereo and Beyond + +Welcome to one of the most exciting parts of our spatial computing journey! Up to this point, we've focused on the "Vulkan-to-OpenXR" plumbing—how to get images, how to time our frames, and how to track user input. Now, we're shifting our focus into the **Shaders**. In spatial computing, we aren't just rendering to a single flat screen; we're rendering for two eyes, and potentially even more views for foveated or wide-FOV headsets. + +The traditional way to handle stereo rendering was to simply draw the entire scene twice—once for the left eye and once for the right. While this is conceptually simple, it's incredibly inefficient. The CPU has to submit twice as many draw calls, and the GPU has to process the same geometry twice, often with nearly identical vertex data. This is where **Single-Pass Stereo** comes in, and more specifically, the **N-View** mindset. + +[source,slang] +---- +// The "N-View" mindset in Slang +void main(uint viewID : SV_ViewID) { + // Shaders that know which eye they are rendering for + float4 eyePosition = loadEyePosition(viewID); +} +---- + +In this chapter, we're going to explore how **Slang** makes authoring these multi-view shaders much more natural and productive than traditional GLSL or HLSL. We'll start by looking at **Native Multiview** (`VK_KHR_multiview`), a core Vulkan feature that allows the GPU to broadcast a single draw call to multiple layers of an image array, each representing a different view. + +We'll define some key concepts like **SIMD** (Single Instruction, Multiple Data) and how it relates to our "N-View" architecture, and we'll see how Slang's modern syntax allows us to write shaders that are clean, readable, and highly optimized for spatial hardware. Whether you're targeting a simple mobile VR headset or a high-end desktop AR system, the principles of efficient multi-view shader design remain the same. + +In the following sections, we'll dive deep into the implementation of **Native Multiview** and then see how to architect our **Slang** shaders to take full advantage of this hardware-level optimization. + +xref:../07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Previous] | xref:02_native_multiview.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc new file mode 100644 index 000000000..6c21365ba --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc @@ -0,0 +1,66 @@ +:pp: {plus}{plus} += Native Multiview + +The core technology behind **Single-Pass Stereo** is **Native Multiview** (`VK_KHR_multiview`). This feature allows a single draw call to be broadcast to multiple layers of an image array. For spatial computing, where we almost always have at least two views (left and right eye), this is a massive performance win. + +== The CPU Overhead Win: Why We Hate Loops + +To understand the value of multiview, look at the alternative: **The Multi-Pass Loop**. + +1. **Loop**: For each eye (0 to 1): +2. **Bind**: Update the view/projection UBO for that eye. +3. **Draw**: Re-issue all 2,000 draw calls for the scene. + +In a complex scene, this means the CPU is doing twice the work (4,000 draw calls total). This often makes the application **CPU-Bound**, meaning the GPU is sitting idle while waiting for the CPU to finish its second loop. + +**Multiview kills the loop.** You issue the 2,000 draw calls *once*, and the GPU's fixed-function hardware handles the duplication for the second eye. The CPU overhead is cut in half, and the GPU can stay saturated. + +== View Masks and the Broadcast Mechanism + +In a standard Vulkan pipeline, a draw call targets a specific attachment. With multiview enabled (part of the Vulkan 1.4 core), you specify a **View Mask** in your `vk::RenderingInfo`. + +A view mask is a bitmask where each set bit represents a layer in the output attachment array. + +* **Mask 0b01 (1)**: Renders only to Layer 0. +* **Mask 0b11 (3)**: Renders to both Layer 0 (Left Eye) and Layer 1 (Right Eye). +* **Mask 0b1111 (15)**: Renders to four layers (used for Quad-Views). + +[source,cpp] +---- +// 1. Enabling Multiview in Vulkan 1.4 Dynamic Rendering +vk::RenderingInfo renderingInfo; +renderingInfo.viewMask = 0b11; // Broadcast to bit 0 and bit 1 +---- + +== The Hardware Implementation: SV_ViewID + +When multiview is active, the GPU executes the vertex shader for each bit set in the mask. Within the shader, you access the current view index using the `SV_ViewID` semantic. + +[source,slang] +---- +// 2. Using the View ID in a Slang Shader +struct VertexInput { + float3 position : POSITION; +}; + +struct VertexOutput { + float4 position : SV_Position; + uint viewID : SV_ViewID; +}; + +VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { + VertexOutput output; + // Load the correct eye matrix using the viewID + // The GPU automatically runs this shader once per viewID (0 and 1) + float4x4 mvp = eyeMatrices[viewID]; + output.position = mul(mvp, float4(input.position, 1.0)); + output.viewID = viewID; + return output; +} +---- + +It's important to understand that while the vertex work is duplicated, the **Primitive Assembly** and **Rasterization** stages can often be optimized by the hardware. Because the two eyes are looking at the same geometry from slightly different angles, the GPU can reuse vertex cache data, making single-pass stereo much more efficient than two separate passes. + +In the next section, we'll see how to architect our **Slang** shaders to manage this "N-View" logic efficiently. + +xref:01_introduction.adoc[Previous] | xref:03_slang_architecture.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc new file mode 100644 index 000000000..92c1da36a --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc @@ -0,0 +1,61 @@ +:pp: {plus}{plus} += Slang Architecture for N-View Shaders + +Authoring multiview-aware shaders is a unique challenge. While the GPU's hardware handles the duplication of draw commands, it's our job as shader developers to ensure that we're accessing our eye-specific data—like view and projection matrices—in a way that is both efficient and organized. This is where the **Slang** language really shines. + +== The N-View Mindset: Agnostic Shaders + +In a traditional shader, you might pass a single `viewMatrix`. In an XR-aware engine, we must adopt an **N-View Mindset**. We write our shaders once, assuming they might be used for mono, stereo, or even "Quad-Views" (4 eyes). + +Slang's powerful type system and **StructuredBuffers** allow us to pass an array of view data that the shader can index into dynamically based on which eye is currently being processed. + +[source,slang] +---- +// 1. Defining a View Structure in Slang +struct SpatialView { + float4x4 viewProjection; + float4x4 inverseView; + float4 eyePosition; +}; + +// 2. Accessing N-Views in a Structured Buffer +// This buffer contains data for all active views (e.g., Left, Right, Inset Left, Inset Right) +StructuredBuffer eyeViews; + +struct VertexInput { + float3 position : POSITION; +}; + +struct VertexOutput { + float4 position : SV_Position; + uint viewID : SV_ViewID; +}; + +VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { + VertexOutput output; + + // Slang makes it easy to access the current eye view data + // The compiler and hardware ensure that 'viewID' matches the current broadcast layer + SpatialView currentView = eyeViews[viewID]; + + output.position = mul(currentView.viewProjection, float4(input.position, 1.0)); + output.viewID = viewID; + return output; +} +---- + +== Propagation to the Fragment Shader + +The `SV_ViewID` isn't just for the vertex shader. You can propagate it down to the **Fragment Shader** to perform eye-specific logic. + +Why would you do this? Consider **Specular Highlights**. Since each eye is at a slightly different physical location, the reflections on a shiny surface should be slightly different for each eye. By using the `eyePosition` from our `SpatialView` structure (indexed by `viewID`), we can calculate the correct view-dependent lighting for each eye in a single pass. + +== Modular Spatial Shaders + +Slang's support for **Generics** and **Interfaces** means we can write shaders that are completely agnostic of the number of views. We can define our spatial logic once and then let the compiler handle the underlying details of the multiview integration. + +This significantly reduces the chances of errors. For example, if you add a third view for a "Spectator Camera" or a "Rear View Mirror," you don't need to change your shader code—you just update your view mask and the size of your `eyeViews` buffer. + +In the next chapter, we'll see how to push this "N-View" mindset even further by looking at **Quad-Views** and **Foveated Rendering**. + +xref:02_native_multiview.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..499d56f72 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc @@ -0,0 +1,76 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Adopting Multiview in our `simple_game_engine` allows us to render both eyes in a single pass, significantly reducing CPU draw call overhead. An example of this implementation can be found in link:../../../attachments/openxr_engine/shaders/pbr.slang[pbr.slang] and link:../../../attachments/openxr_engine/renderer_pipelines.cpp[renderer_pipelines.cpp]. + +== Updating Shader Loading + +In `renderer_resources.cpp`, we need to update our pipeline creation to enable multiview. We do this by adding `vk::PipelineRenderingCreateInfo` with the `viewMask` set. + +[source,cpp] +---- +// renderer_resources.cpp +bool Renderer::createGraphicsPipeline() { + // ... + vk::PipelineRenderingCreateInfo renderingInfo{ + .viewMask = xrMode ? 0x3 : 0x0, // 0x3 enables views 0 and 1 + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat + }; + + pipelineInfo.pNext = &renderingInfo; + // ... +} +---- + +== Adapting Slang Shaders + +Our Slang shaders (like `pbr.slang`) must be updated to handle the `SV_ViewID` semantic. This allows the shader to distinguish between the left and right eye. + +First, we update our `common_types.slang` to include an array of matrices in our `UniformBufferObject`: + +[source,slang] +---- +// common_types.slang +struct UniformBufferObject { + // NEW: Replace the single viewProjection matrix with an array + // This supports Stereo (N=2), Quad-Views (N=4), and beyond + float4x4 viewProjections[4]; + float4x4 model; +}; +---- + +Next, in `pbr.slang`: + +[source,slang] +---- +// pbr.slang +struct VS_Input { + float3 position : POSITION; + // ... +}; + +struct VS_Output { + float4 position : SV_Position; + uint viewID : SV_ViewID; // NEW: The hardware-provided view index + // ... +}; + +[shader("vertex")] +VS_Output vertexMain(VS_Input input, uint viewID : SV_ViewID) { + VS_Output output; + + // Select the correct ViewProjection matrix based on the eye index (0 to 3) + float4x4 vp = ubo.viewProjections[viewID]; + output.position = mul(vp, mul(pushConstants.model, float4(input.position, 1.0))); + output.viewID = viewID; + + return output; +} +---- + +== Why These Changes? + +By enabling the `viewMask` and using `SV_ViewID`, we shift the burden of stereo rendering from the CPU to the GPU. Instead of the engine looping twice and submitting two sets of commands, it submits one set, and the hardware's **Instance Data Step** or **Geometry Replication** handles the expansion to two views. This is the most efficient way to achieve high-performance spatial rendering in Vulkan 1.4. + +xref:03_slang_architecture.adoc[Previous] | xref:../09_Quad_Views_Foveated/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc new file mode 100644 index 000000000..351c30e77 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc @@ -0,0 +1,14 @@ +:pp: {plus}{plus} += Quad-Views and Foveated Rendering: High-Fidelity Spatial Computing + +As spatial computing hardware continues to advance, we're seeing headsets that push the boundaries of resolution and field-of-view (FOV). To maintain high performance while delivering incredible detail, many modern systems utilize **Quad-Views** and **Foveated Rendering**. These techniques move beyond simple two-eye stereo to a more sophisticated "N=4" model of rendering. + +The core idea is simple: the human eye only sees high detail in a very small central area called the **fovea**. Outside of that area, our vision is much lower resolution. By dedicating more rendering power to the foveal region and less to the peripheral area, we can significantly reduce the overall GPU workload without the user noticing a loss in quality. This is the essence of **Foveated Rendering**. + +In this chapter, we're going to explore how to architect our spatial engine to handle these multi-layer rendering setups. We'll start by looking at **Quad-Views**, where each eye receives two distinct views: a high-resolution "inset" (the foveal view) and a lower-resolution "base" (the peripheral view). This results in a total of four views (N=4) that need to be synchronized and composed. + +We'll define some key terms like **PPS** (Pixel per Degree) and see how to manage the **Swapchain** submission for these multiple layers in OpenXR. Whether you're targeting high-end PC VR or the latest mobile spatial systems, understanding how to manage multi-layer composition is a vital skill for any advanced spatial developer. + +In the following sections, we'll dive into the details of architecting these primary and inset view layers, and we'll see how to efficiently submit them to the OpenXR compositor for the final image. + +xref:../08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Previous] | xref:02_primary_stereo_with_insets.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc new file mode 100644 index 000000000..70184717d --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc @@ -0,0 +1,60 @@ +:pp: {plus}{plus} += Primary Stereo with Insets + +In a **Quad-View** architecture, each eye is actually seeing two overlapping images. The **Primary View** is a wide, lower-resolution image that covers the user's entire field-of-view (FOV). The **Inset View** is a smaller, high-resolution image that is centered around the user's gaze. + +== The Pixel Density Problem + +To understand why we use quad-views, we must consider the hardware limits of modern displays. A typical VR headset might have a 4K resolution per eye spread over a 110-degree FOV. + +* **Result**: This gives a density of roughly 20-25 **PPD** (Pixels Per Degree). +* **The Goal**: Human "Retina" resolution is closer to 60 PPD. + +We cannot simply double the resolution of the entire display—the GPU couldn't handle it. Instead, we use **Foveated Rendering**. We render a high-density "Inset" (60 PPD) only where the eye is looking, and a lower-density "Primary" (20 PPD) for the periphery. + +== Fixed vs. Dynamic Foveation + +1. **Fixed Foveation (FFR)**: The high-detail inset is locked to the center of the lens. This is effective because XR lenses are naturally blurrier at the edges anyway. +2. **Dynamic Foveation (DFR)**: The high-detail inset moves in real-time based on **Eye Tracking** data. This is the "Holy Grail" of XR performance, as the user always sees perfect detail regardless of where they look. + +[source,cpp] +---- +// 1. Defining the View Config for Quad-Views (N=4) +// Indices 0,1 are the wide base views. +// Indices 2,3 are the high-detail foveal insets. +XrViewConfigurationView configViews[4]; +---- + +== Rendering the Quad-View in Slang + +From a rendering perspective, our engine manages four distinct view matrices. The inset views (2 and 3) have a much smaller FOV, which effectively "zooms in" on the scene without changing the camera position. + +[source,slang] +---- +// 2. Slang Shader Handling N=4 Views +struct QuadViewData { + float4x4 viewProjection; + float4 viewportScale; // Scale factor for foveal inset +}; + +StructuredBuffer allViews; // 4 Views total + +struct VertexOutput { + float4 position : SV_Position; + uint viewID : SV_ViewID; +}; + +VertexOutput main(uint viewID : SV_ViewID, float3 pos : POSITION) { + VertexOutput output; + QuadViewData current = allViews[viewID]; + + // The GPU broadcasts the same geometry to 4 different viewports/layers + output.position = mul(current.viewProjection, float4(pos, 1.0)); + output.viewID = viewID; + return output; +} +---- + +Crucially, the **Inset View** isn't just a zoomed-in version of the base view. It's a high-density image that captures more detail from the 3D scene. We are effectively focusing the GPU's rasterization power on a smaller subset of the world. + +xref:01_introduction.adoc[Previous] | xref:03_multi_layer_composition.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc new file mode 100644 index 000000000..0d62156c4 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc @@ -0,0 +1,49 @@ +:pp: {plus}{plus} += Multi-Layer Composition + +Rendering a **Quad-View** is only half the battle. Once we have our four views (base eyes and high-detail insets), we need to get them to the **OpenXR Compositor** for the final display. This is where **Multi-Layer Composition** comes in. + +== The Layer Stack: Modular Imagery + +In OpenXR, you don't just submit a single image. You submit an array of **Composition Layers**. Think of the compositor as a "Super-GPU" that performs a final, high-priority pass to stack these images together before they hit the display. + +1. **Base Layer**: Your standard 3D world (usually indices 0 and 1 of your quad-view). +2. **Inset Layer**: The high-detail foveal data (indices 2 and 3). +3. **Overlay Layer**: A flat HUD or menu that stays perfectly sharp, regardless of the 3D scene's resolution. + +[source,cpp] +---- +// 1. Defining Composition Layers in OpenXR +XrCompositionLayerProjection baseLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; +XrCompositionLayerProjection insetLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; + +// 2. Submitting both layers to xrEndFrame +// The order matters: the last layer in the list is drawn on top. +std::vector layers; +layers.push_back(reinterpret_cast(&baseLayer)); +layers.push_back(reinterpret_cast(&insetLayer)); + +XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; +frameEndInfo.layerCount = layers.size(); +frameEndInfo.layers = layers.data(); +xrEndFrame(session, &frameEndInfo); +---- + +== Masking and Blending + +How does the compositor know where to show the high-detail inset? Each layer specifies a **Projection View**. For quad-views, the inset layer's views will have a smaller FOV but the same camera pose. + +The compositor uses the FOV tangents to mathematically "place" the high-detail image over the low-detail one. It then performs a **Soft Edge Blend** to ensure the transition between the 20 PPD and 60 PPD regions is invisible to the user. + +== Resource Synchronization: Multi-Swapchain Pacing + +From an engine perspective, this means we are now managing **Multiple Swapchains** per frame. + +* **Pacing**: We must call `xrWaitSwapchainImage` and `xrAcquireSwapchainImage` for *each* swapchain (Base and Inset). +* **Vulkan Sync**: Our command buffer recording must ensure that all four views are rendered and transitioned before we call `xrReleaseSwapchainImage`. + +This multi-layer approach is incredibly flexible. Each layer can even have its own resolution and bit-depth, allowing the compositor to optimize the final image based on the specific needs of each part of the scene. + +In the next chapter, we'll see how to push our performance even further by looking at **Variable Rate Shading** (VRS). + +xref:02_primary_stereo_with_insets.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..36be53522 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc @@ -0,0 +1,67 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Supporting Quad-Views (N=4) in our `simple_game_engine` requires us to move from a single-layer swapchain to a multi-layer composition in `renderer_rendering.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Multi-Layer Swapchain Composition + +In `renderer_rendering.cpp`, we update `Render` to submit multiple projection layers to OpenXR. Each eye will have a peripheral (wide FOV) and a foveal (narrow, high-res) layer. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... + if (xrMode) { + // Define two layers for the XR compositor: + // Layer 0: Peripheral (Wide FOV) + // Layer 1: Foveal (Narrow FOV, High-Res) + std::vector layers; + + for (uint32_t eye = 0; eye < 2; ++eye) { + // Retrieve views from OpenXR (N=4) + // xrContext provides these based on xrLocateViews + auto const& views = xrContext.getQuadViews(); + + // Populate peripheral layer + layers[0].views[eye].pose = views[0 + eye].pose; + layers[0].views[eye].fov = views[0 + eye].fov; + layers[0].views[eye].subImage.swapchain = *xrPeripheralSwapchain; + + // Populate foveal layer (the "inset") + layers[1].views[eye].pose = views[2 + eye].pose; + layers[1].views[eye].fov = views[2 + eye].fov; + layers[1].views[eye].subImage.swapchain = *xrFovealSwapchain; + } + + // Submit multi-layer composition to OpenXR + xrContext.submitLayers(layers); + } +} +---- + +== Handling High-Fidelity Insets + +In our `createGraphicsPipeline`, we ensure our `viewMask` is expanded if we are rendering all 4 views in a single pass. + +[source,cpp] +---- +// renderer_resources.cpp +bool Renderer::createGraphicsPipeline() { + // ... + vk::PipelineRenderingCreateInfo renderingInfo{ + .viewMask = xrMode ? 0xF : 0x0, // 0xF enables views 0, 1, 2, 3 + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat + }; + + pipelineInfo.pNext = &renderingInfo; + // ... +} +---- + +== Why These Changes? + +By moving to a multi-layer composition, we enable "Foveal Rendering" where the engine renders the center of the user's view at a much higher resolution than the periphery. Because our engine's `Render` loop is already dynamic, it can seamlessly switch between these views and layers, ensuring that the user always experiences the highest fidelity where they are actually looking. + +xref:03_multi_layer_composition.adoc[Previous] | xref:../10_Variable_Rate_Shading/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc new file mode 100644 index 000000000..290df0a66 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc @@ -0,0 +1,16 @@ +:pp: {plus}{plus} += Variable Rate Shading (VRS) for Peripheral Optimization + +The core challenge of high-fidelity spatial computing is the sheer number of pixels we need to push. With headsets moving toward 4K-per-eye resolutions at 90Hz or 120Hz, simply rendering the entire scene at full resolution is often computationally prohibitive. However, human vision provides us with a unique optimization opportunity: **Foveated Rendering**. + +Our eyes only perceive high detail in a very small central area called the **fovea**. As we move toward the periphery of our vision, our ability to resolve fine detail drops off significantly. In this chapter, we will explore how to leverage **Variable Rate Shading (VRS)**—a core feature of Vulkan 1.4—to intelligently reduce fragment processing in these peripheral areas without sacrificing perceived quality. + +Variable Rate Shading allows us to decoupling the shading rate from the pixel rate. Instead of running a fragment shader once for every pixel, we can tell the hardware to run it once for a group of pixels (e.g., a 2x2 or 4x4 tile). This "coarse shading" significantly reduces the **ALU** (Arithmetic Logic Unit) load on the GPU, which is often the primary bottleneck in complex spatial shaders. + +We will focus on two primary strategies: +1. **Static Peripheral Optimization**: Reducing shading rates at the edges of the lens where optical distortion and chromatic aberration already obscure detail. +2. **Dynamic Gaze-Driven Shading**: Using eye-tracking telemetry to center the high-resolution region wherever the user is currently looking. + +By the end of this chapter, you will understand how to integrate the **VK_KHR_fragment_shading_rate** extension (now part of the Vulkan 1.4 core) into your spatial pipeline and how to manage shading rate maps that update in real-time. + +xref:../09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Previous] | xref:02_fragment_density_control.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc new file mode 100644 index 000000000..49785b65b --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc @@ -0,0 +1,44 @@ +:pp: {plus}{plus} += Fragment Density Control and Vulkan 1.4 VRS + +In legacy pipelines, reducing shading quality often meant rendering to a lower-resolution buffer and upscaling, which introduced blurring and aliasing artifacts. With Vulkan 1.4, we have a much more elegant solution: **Variable Rate Shading (VRS)**. + +== The Concept: Decoupling Rasterization from Shading + +To understand VRS, you must distinguish between two parts of the graphics pipeline: + +1. **Rasterization**: The process of determining which pixels are covered by a triangle. +2. **Shading**: The process of running a fragment shader to determine the color of those pixels. + +In a standard pipeline, they are 1:1. For every pixel covered, you run one fragment shader. With **VRS**, you can run one fragment shader for a *group* of pixels (e.g., a 2x2 or 4x4 block) while still keeping the high-resolution edges and depth data from the rasterizer. + +This allows us to maintain **Geometric Sharpness** (no aliasing on object edges) while significantly reducing the **ALU** (Arithmetic Logic Unit) cost of complex lighting and materials in areas where detail isn't needed. + +== Shading Rate Images: The Density Map + +In a spatial pipeline, we use a **Shading Rate Image**. This is a low-resolution attachment (usually 1/16th the size of the main view) where each "pixel" represents a tile of the screen. + +* **Value 0x0**: 1x1 shading (Full quality). +* **Value 0x5**: 2x2 shading (1/4th the cost). +* **Value 0xA**: 4x4 shading (1/16th the cost). + +[source,cpp] +---- +// Configuring the shading rate state for a spatial pipeline +vk::PipelineFragmentShadingRateStateCreateInfoKHR shadingRateState{}; +shadingRateState.fragmentSize = {1, 1}; // Default fallback +// We "Replace" the pipeline rate with the rate from our density image +shadingRateState.combinerOps[0] = vk::FragmentShadingRateCombinerOpKHR::eReplace; +shadingRateState.combinerOps[1] = vk::FragmentShadingRateCombinerOpKHR::eKeep; + +vk::GraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.pNext = &shadingRateState; +---- + +== Fixed Vignette Shading + +Because XR lenses are naturally bluer and more distorted at the edges, we can pre-calculate a "Vignette" map. The center of the lens (the "Sweet Spot") is set to 1x1, while the outer rings are set to 2x2 or 4x4. Since the user rarely looks at the extreme edges of the lens, this is essentially "Free Performance." + +When combined with **Multiview**, a single shading rate map can be applied to both eyes simultaneously, saving massive amounts of GPU time without affecting the user's immersion. + +xref:01_introduction.adoc[Previous] | xref:03_gaze_driven_logic.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc new file mode 100644 index 000000000..0eff80b4c --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc @@ -0,0 +1,43 @@ +:pp: {plus}{plus} += Gaze-Driven Logic and Eye-Tracking Telemetry + +While static peripheral optimization is a great baseline, the real power of foveated rendering comes when we align the high-detail region with the user's actual gaze. This is known as **Dynamic Foveated Rendering (DFR)**. To achieve this, we ingest eye-tracking data from the OpenXR runtime via the **XR_EXT_eye_gaze_interaction** extension. + +== The Biological Exploit: Saccadic Suppression + +To understand why DFR is so effective, we must look at how the human brain processes visual information. Our eyes are constantly making tiny, rapid movements called **Saccades**. During a saccade, the brain actually "blanks out" visual input for a fraction of a second—a phenomenon called **Saccadic Suppression**. + +DFR exploits this. By using high-speed eye tracking (usually 120Hz or higher), our engine can shift the high-detail foveal region to the user's new gaze point during the saccade itself. The user never "catches" the low-resolution periphery because their brain is literally ignoring visual input while the eye is moving. + +== Implementing Gaze-Driven VRS + +The process involves three steps: + +1. **Ingestion**: Querying the `XrEyeGaze` pose to find where the user is looking in 3D space. +2. **Projection**: Projecting that 3D gaze vector onto the 2D plane of the viewport to find the "foveal center" in UV coordinates. +3. **Update**: Regenerating or shifting the **Shading Rate Image** so that the 1x1 shading region follows that center. + +[source,cpp] +---- +// Updating the shading rate image based on projected gaze coordinates +void updateShadingRateMap(vk::raii::CommandBuffer& cmd, const glm::vec2& gazeCenter) { + // We use a simple compute shader to fill a small R8_UINT texture. + // The shader calculates distance from gazeCenter and assigns: + // 0x0 (1x1 shading), 0x4 (2x1), 0x5 (2x2), etc. + cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *gazeVrsPipeline); + cmd.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eCompute, 0, gazeCenter); + cmd.dispatch(shadingRateWidth / 8, shadingRateHeight / 8, 1); +} +---- + +== Latency: The Immersion Killer + +Human eyes move incredibly fast. If your eye-tracking telemetry or your shading rate update has too much latency, the user will see the blurriness at the edge of their vision before the high-detail patch "catches up." + +This is why **Late Latching** (which we covered in Chapter 6) is equally important here. We want to sample the eye gaze at the absolute last microsecond before we signal the timeline semaphore to start the GPU work. By minimizing the latency between "where the eye is" and "where the pixels are sharp," we maintain the illusion of a perfectly high-resolution world. + +When implemented correctly, DFR allows you to push the boundaries of visual fidelity, enabling effects like heavy path-tracing or complex volumetrics that would be impossible to run at full resolution across the entire screen. + +In the next chapter, we will shift from software-driven optimization to hardware-driven geometry by looking at **Canted Displays and Asymmetric Frustums**. + +xref:02_fragment_density_control.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..dbdb0f4b6 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc @@ -0,0 +1,82 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Integrating **Variable Rate Shading (VRS)** into our `simple_game_engine` allows us to unlock a massive performance boost by being smarter about how we process fragments in `renderer_resources.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Enabling VRS in the Backend + +In `renderer_core.cpp`, we must enable the VRS features during device creation. + +[source,cpp] +---- +// renderer_core.cpp +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + // ... + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + + // NEW: Enable Fragment Shading Rate (VRS) + vk::PhysicalDeviceFragmentShadingRateFeaturesKHR vrsFeatures; + vrsFeatures.attachmentFragmentShadingRate = vk::True; + vrsFeatures.pipelineFragmentShadingRate = vk::True; + + vulkan13Features.pNext = &vrsFeatures; + // ... +} +---- + +== Dynamic Gaze-Driven VRS + +In `renderer_rendering.cpp`, we update our `Render` loop to bind a shading rate image that highlights the user's current gaze. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... + if (xrMode) { + // Query current gaze from OpenXR (Eye Tracking) + glm::vec2 gazeNDC = xrContext.getGazeNDC(); + + // Update the shading rate image (a low-res 8-bit image) + // High density (1x1) at gazeNDC, low density (4x4) at periphery + updateShadingRateImage(gazeNDC); + + // Bind the shading rate attachment to our dynamic rendering info + vk::RenderingFragmentShadingRateAttachmentInfoKHR vrsAttachment{ + .imageView = *vrsImageView, + .imageLayout = vk::ImageLayout::eFragmentShadingRateAttachmentOptimalKHR, + .shadingRateAttachmentTexelSize = vk::Extent2D(16, 16) + }; + + passInfo.pNext = &vrsAttachment; + } + + commandBuffer.beginRendering(passInfo); + // ... +} + +void Renderer::updateShadingRateImage(glm::vec2 gazeNDC) { + // 1. Map NDC (-1 to 1) to our shading rate image texels + // The image is low-res (e.g., 1/16th of the main display) + uint32_t gazeX = static_cast((gazeNDC.x * 0.5f + 0.5f) * vrsWidth); + uint32_t gazeY = static_cast((gazeNDC.y * 0.5f + 0.5f) * vrsHeight); + + // 2. Fill the image: 1x1 (full rate) at gaze, 4x4 (low rate) at periphery + // We use a simple distance-based falloff + for (uint32_t y = 0; y < vrsHeight; ++y) { + for (uint32_t x = 0; x < vrsWidth; ++x) { + float dist = glm::distance(glm::vec2(x, y), glm::vec2(gazeX, gazeY)); + vrsData[y * vrsWidth + x] = (dist < foveaRadius) ? 0x0 : 0x4; // 0x0=1x1, 0x4=4x4 + } + } + // 3. Upload to GPU + vrsBuffer.upload(vrsData); +} +---- + +== Why These Changes? + +By integrating eye-tracking data directly into our Vulkan 1.4 rendering pass, we enable **Dynamic Foveated Rendering (DFR)**. This allows the engine to save up to 60% of fragment processing costs in complex PBR scenes by only rendering the tiny area the user is actually looking at in full detail. Because our engine uses dynamic rendering, adding the `vrsAttachment` to our `passInfo` is a simple `pNext` extension away. + +xref:03_gaze_driven_logic.adoc[Previous] | xref:../11_Canted_Displays/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc new file mode 100644 index 000000000..833836a14 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc @@ -0,0 +1,15 @@ +:pp: {plus}{plus} += Canted Displays and Asymmetric Frustums + +In a standard desktop renderer, we typically assume that the two virtual cameras (the "eyes") are parallel to each other. This is known as a **Parallel Projection** model. However, many modern high-end headsets—such as the Pimax or Valve Index—utilize a **Canted Display** architecture. In these devices, the physical screens and lenses are tilted outward to provide a wider **Field of View (FOV)**. + +This mechanical canting introduces a significant layer of complexity to our rendering pipeline. We can no longer assume that the view matrices are simple lateral offsets of each other. Instead, each eye has its own unique orientation and an **Asymmetric Frustum**—where the focal point is not in the center of the image. + +In this chapter, we will explore how to handle these non-parallel architectures by: +1. **Decomposing the XR Pose**: Understanding how to use `XrView` data to build proper 4x4 matrices that account for the canting angle. +2. **Calculating Asymmetric Frustums**: Using the OpenXR `fov` tangents to build projection matrices where the "center" is shifted to align with the optical axis. +3. **Optimizing with Viewport Swizzling**: Utilizing the **VK_NV_viewport_swizzle** extension (or its Vulkan 1.4 equivalents) to optimize how these non-rectilinear projections are rasterized. + +By mastering these concepts, your engine will be compatible with the entire spectrum of spatial hardware, from mobile VR headsets with parallel screens to ultra-wide FOV enthusiast hardware. + +xref:../10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Previous] | xref:02_non_parallel_projections.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc new file mode 100644 index 000000000..32f78aa01 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc @@ -0,0 +1,52 @@ +:pp: {plus}{plus} += Calculating Asymmetric Frustums for Wide-FOV Lenses + +When you query `xrLocateViews`, the OpenXR runtime returns two pieces of information for each eye: a `pose` (position and orientation) and a `fov` (field of view). While many developers are tempted to simply use a fixed FOV and an eye-offset, this approach will fail on headsets with canted lenses. + +== The Tangent Frustum: Mapping Angles to Pixels + +To understand why OpenXR uses tangents, we must look at how a camera sees the world. A standard field of view is just an angle. But GPUs work with **Clip Space**—a box from -1 to 1. To get from a 3D world to a 2D box, we need the tangents of the FOV angles. + +The `fov` provided by OpenXR is expressed as four tangents: `angleLeft`, `angleRight`, `angleUp`, and `angleDown`. + +* **Symmetric (Desktop)**: Left = -Right, Up = -Down. The optical center is in the middle. +* **Asymmetric (XR)**: Left != -Right. The optical center is shifted to align with the physical lens. + +== Building the Matrix + +To build a proper 4x4 projection matrix from these tangents using `vulkan-hpp`, we use the following logic: + +[source,cpp] +---- +// Constructing an asymmetric projection matrix from OpenXR FOV tangents +glm::mat4 createProjectionMatrix(const XrFovf& fov, float zNear, float zFar) { + const float tanLeft = tan(fov.angleLeft); + const float tanRight = tan(fov.angleRight); + const float tanUp = tan(fov.angleUp); + const float tanDown = tan(fov.angleDown); + + const float tanWidth = tanRight - tanLeft; + const float tanHeight = tanUp - tanDown; + + glm::mat4 projection(0.0f); + projection[0][0] = 2.0f / tanWidth; + projection[1][1] = 2.0f / tanHeight; + + // The "Shift" components: These align the frustum with the lens optical center + projection[2][0] = (tanRight + tanLeft) / tanWidth; + projection[2][1] = (tanUp + tanDown) / tanHeight; + + projection[2][2] = zFar / (zNear - zFar); + projection[2][3] = -1.0f; + projection[3][2] = -(zFar * zNear) / (zFar - zNear); + return projection; +} +---- + +== Canted Displays: The Rotation Problem + +On headsets like the Valve Index or Pimax, the display panels themselves are rotated outward (canted). This means your **View Matrices** (the eye's position and rotation) are also different for each eye. + +When you use **Multiview**, you must push an array of projection matrices and an array of view matrices to your Slang shaders. If you try to use a single "stereo" matrix with a fixed offset, the world will appear warped and uncomfortable because the GPU is drawing a rectilinear world for a non-rectilinear display. + +xref:01_introduction.adoc[Previous] | xref:03_viewport_swizzling.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc new file mode 100644 index 000000000..42de694c6 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc @@ -0,0 +1,40 @@ +:pp: {plus}{plus} += Optimizing Projections with Viewport Swizzling + +In some wide-FOV headsets, the physical display panel may be mounted at an angle that does not align with the standard Cartesian coordinate system. This is where the **VK_NV_viewport_swizzle** extension (or the modern Vulkan 1.4 equivalents) becomes invaluable. + +== The Swizzle Stage: Re-routing the Pipeline + +To understand viewport swizzling, you must look at the hardware path of a pixel. After the vertex shader finishes, the GPU performs **Perspective Division** and **Viewport Transformation**. Usually, this is fixed: X goes to X, Y goes to Y. + +With **Viewport Swizzling**, you can re-route these components *at the hardware level*. + +* **Why?**: Some headsets use rotated display panels to fit more pixels into a specific FOV. +* **Shader Problem**: Flipping X and Y in a shader is easy, but it happens *before* rasterization, which can break depth testing and hardware culling. +* **Hardware Solution**: Swizzling happens *after* the shader but *before* the pixel is drawn. It allows the hardware to handle rotated or inverted axes with zero performance penalty. + +[source,cpp] +---- +// Configuring a viewport swizzle for non-standard display layouts +vk::PipelineViewportSwizzleStateCreateInfoNV swizzleState{}; +vk::ViewportSwizzleNV leftEyeSwizzle{ + vk::ViewportCoordinateSwizzleNV::ePositiveX, + vk::ViewportCoordinateSwizzleNV::eNegativeY, // Invert vertical axis for hardware-level flip + vk::ViewportCoordinateSwizzleNV::ePositiveZ, + vk::ViewportCoordinateSwizzleNV::ePositiveW +}; +swizzleState.pViewportSwizzles = &leftEyeSwizzle; + +vk::GraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.pNext = &swizzleState; +---- + +== Device Specificity and Portability + +While viewport swizzling is a powerful tool for low-level hardware optimization, it is important to remember that it is often **vendor-specific** (like the `NV` suffix suggests). + +In a modern spatial engine, you should treat swizzling as a "Hardware Driver" feature. Your engine's core logic should remain rectilinear, but your `VulkanContext` can apply these swizzles based on the headset's requirements discovered during the handshake. This keeps your game code clean while still hitting the absolute maximum performance on exotic wide-FOV hardware. + +In the next chapter, we will look at how to scale these spatial techniques to even larger environments using **CAVE Architecture and Multi-GPU Synchronization**. + +xref:02_non_parallel_projections.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..ed6be658d --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc @@ -0,0 +1,62 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +The introduction of **Canted Displays** and **Asymmetric Frustums** requires us to move away from the "parallel eyes" assumption in our `simple_game_engine`'s camera system. An example of this implementation can be found in link:../../../attachments/openxr_engine/camera_component.cpp[camera_component.cpp]. + +== Evolving the Camera System + +In `camera.cpp`, we must update how we calculate the projection matrix to handle the asymmetric FOVs provided by OpenXR. + +[source,cpp] +---- +// camera.cpp +glm::mat4 Camera::getAsymmetricProjection(const XrFovf& fov, float nearZ, float farZ) { + float tanLeft = std::tan(fov.angleLeft); + float tanRight = std::tan(fov.angleRight); + float tanUp = std::tan(fov.angleUp); + float tanDown = std::tan(fov.angleDown); + + float tanWidth = tanRight - tanLeft; + float tanHeight = tanUp - tanDown; + + // Build the asymmetric projection matrix + glm::mat4 projection(0.0f); + projection[0][0] = 2.0f / tanWidth; + projection[1][1] = 2.0f / tanHeight; + projection[2][0] = (tanRight + tanLeft) / tanWidth; + projection[2][1] = (tanUp + tanDown) / tanHeight; + projection[2][2] = -farZ / (farZ - nearZ); + projection[2][3] = -1.0f; + projection[3][2] = -(farZ * nearZ) / (farZ - nearZ); + + return projection; +} +---- + +== Handling Canted View Matrices + +In `engine.cpp`, when we update the camera, we must ensure we use the full 6-DOF pose for each eye, not just a simple X-offset. + +[source,cpp] +---- +// engine.cpp +void Engine::Update(XrTime predictedTime) { + // ... + auto [leftView, rightView] = xrContext.locateViews(predictedTime); + + // Instead of a single offset, we pass the full view matrices + // which may include rotation (canting) + activeCamera->setEyeViewMatrix(0, xrPoseToMatrix(leftView.pose)); + activeCamera->setEyeViewMatrix(1, xrPoseToMatrix(rightView.pose)); + + // Set the asymmetric projections we calculated above + activeCamera->setEyeProjectionMatrix(0, getAsymmetricProjection(leftView.fov, ...)); + activeCamera->setEyeProjectionMatrix(1, getAsymmetricProjection(rightView.fov, ...)); +} +---- + +== Why These Changes? + +By supporting canted views and asymmetric frustums, our engine becomes compatible with high-end "enthusiast" headsets. Proper projection matrix calculation is the difference between a world that looks "right" and one that feels slightly distorted or "warped" as you move your head. Because we've already integrated the OpenXR handshake, our `xrContext` can provide the exact FOV tangents and poses required for these calculations. + +xref:03_viewport_swizzling.adoc[Previous] | xref:../12_CAVE_Architecture/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc new file mode 100644 index 000000000..f760d042f --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc @@ -0,0 +1,28 @@ +:pp: {plus}{plus} += CAVE Architecture and Multi-GPU Synchronization: Introduction + +While most of our focus so far has been on head-mounted displays (HMDs), spatial computing also encompasses large-scale environments like **CAVEs** (Cave Automatic Virtual Environments). A CAVE is a room where the walls, floor, and sometimes the ceiling are high-resolution projection surfaces. The user typically wears lightweight shutter glasses and is tracked by a system of cameras, creating a shared, immersive 3D space. + +Architecting an engine for a CAVE is fundamentally different from a single-GPU headset. To drive six or more 4K projectors at 120Hz, we often need a **Networked Cluster** of powerful workstations, each equipped with one or more high-end GPUs. This introduces a massive synchronization challenge: every frame must be displayed on every screen at the exact same microsecond to avoid "tearing" at the corners of the room. + +In this chapter, we will dive into the engineering required for room-scale spatial technology: + +1. **Projector-Based Spatial Tech**: Understanding the geometry of a CAVE and how to architect an engine to handle multiple, non-standard projection surfaces. +2. **Hardware Synchronization**: Utilizing the **VK_NV_present_barrier** extension and **Swap Groups** to ensure frame-perfect synchronization across networked cluster nodes. +3. **Distributed Rendering**: Designing a data-parallel architecture where each node in the cluster renders its own slice of the 3D world while remaining in lock-step with the master simulation. + +== The Challenge of Room-Scale Spatial Tech + +In an HMD, the screens are physically attached to your head. In a CAVE, the screens are the walls. This means that as you move around the room, the **Asymmetric Frustums** for each projection surface must be recalculated in real-time based on your tracked eye position. + +Furthermore, because the images are being stitched together from multiple projectors, we must handle **Geometric Correction** and **Edge Blending**—concepts we will explore in the next chapter—to ensure the virtual world appears seamless as it wraps around the physical corners of the room. + +== Synchronizing the Cluster + +The "holy grail" of CAVE engineering is **Genlock** (Generator Lock) and **Swaplock**. Genlock ensures that the video signal from every GPU is sent to the projectors at the exact same time. Swaplock ensures that the command to swap the front and back buffers happens simultaneously across the entire cluster. + +Without these hardware-level guarantees, the "immersion" of a CAVE is quickly broken by jitter and visual discontinuities. We will look at how Vulkan 1.4 features and specialized hardware extensions allow us to achieve this level of precision. + +By the end of this chapter, you will understand how to scale your spatial engine from a single-user headset to a multi-node, room-scale immersive environment. + +xref:../11_Canted_Displays/04_incorporating_into_the_engine.adoc[Previous] | xref:02_projector_based_spatial_tech.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc new file mode 100644 index 000000000..25784b2a3 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc @@ -0,0 +1,47 @@ +:pp: {plus}{plus} += Architecting for Projector-Based Spatial Environments + +When we transition from a headset to a **CAVE** (Cave Automatic Virtual Environment), the way we define our "cameras" changes fundamentally. In a headset, the cameras are relative to the user's head. In a CAVE, the "cameras" are fixed physical surfaces—the walls—and the user moves *within* them. + +== The Observer Paradox: One Perspective, Many Screens + +To understand CAVE rendering, you must accept a paradox: **The 3D scene is only correct for one person.** + +Because the walls are fixed, if the user moves to the left, the image on the front wall must be skewed to maintain the illusion of depth from their new perspective. This is known as **Off-Axis Projection**. + +1. **Fixed Display**: The projection surface (the wall) never moves. +2. **Moving Eye**: The user's eye (the viewpoint) moves constantly. +3. **Result**: The "Pyramid of Vision" (the frustum) is highly asymmetric and changes its skew every frame based on the user's physical location in the room. + +== Calculating the Off-Axis Frustum + +To calculate this, we treat the physical wall as the "Near Plane" of our camera. We calculate the frustum that passes from the tracked eye through the four corners of the wall. + +[source,cpp] +---- +// A simple representation of a physical projection wall in our engine +struct ProjectionWall { + glm::vec3 bottomLeft; + glm::vec3 bottomRight; + glm::vec3 topLeft; + // We calculate the normal and plane equation from these points +}; + +// The resulting Projection Matrix is built by measuring the distance +// from the eye to the wall's corners in the wall's local coordinate system. +---- + +== The Distributed Rendering Model: Leader and Follower + +Driving a 5-sided CAVE at 4K resolution requires immense GPU power. Most systems use a **Cluster** of computers. + +* **Leader Node**: Runs the main simulation, physics, and input processing. It then broadcasts the "State of the World" (object positions, user pose, system time) to the followers. +* **Follower Nodes**: These nodes perform **Deterministic Simulation**. They receive the packet, update their internal world state to match the leader exactly, and then render only the specific wall or projector they are responsible for. + +== Determinism is Mandatory + +In a cluster, every computer must agree on the state of the world. If a particle system uses a random seed that differs between nodes, one wall will show a particle in a different place than the adjacent wall, breaking the immersion. Every "Random" or "Time-based" element must be synchronized across the network. + +To the user, this cluster behaves as a single, massive GPU. However, this only works if every node renders and displays the exact same frame at the exact same time. + +xref:01_introduction.adoc[Previous] | xref:03_hardware_sync.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc new file mode 100644 index 000000000..efdf8ff72 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc @@ -0,0 +1,52 @@ +:pp: {plus}{plus} += Hardware Synchronization: Swap Groups and Barriers + +In a networked cluster driving a **CAVE**, "close enough" is not good enough for synchronization. If Node A (the left wall) swaps its buffers even one millisecond before Node B (the front wall), the user will see a distracting horizontal line or a "misaligned" object across the corner where the two walls meet. This is known as **Cluster Tearing**. + +== Swap Groups: Intra-Machine Sync + +A **Swap Group** synchronizes multiple GPUs or multiple displays connected to a *single* workstation. + +When one GPU in the group is ready to present, the hardware holds the "Swap" signal until all other GPUs in that same machine are also ready. This ensures that the two outputs of a single dual-GPU workstation always update their pixels at the exact same refresh cycle. + +== Swap Barriers: Inter-Machine Sync + +A **Swap Barrier** is the network extension of a swap group. This works across *multiple* workstations. + +1. **Hardware Connection**: A physical cable (often a BNC or RJ45 sync cable) connects the GPUs across the cluster. +2. **The Handshake**: When Node A finishes its frame, it sends a "Ready" signal over the cable. +3. **The Wait**: Node A blocks its execution. +4. **The Release**: Only when *every* node in the cluster has sent its "Ready" signal does the hardware release the barrier, allowing every GPU to swap their buffers simultaneously. + +== Implementing the Barrier in Vulkan + +We use the **VK_NV_present_barrier** extension to join these hardware groups. + +[source,cpp] +---- +// Checking for Present Barrier support and joining a group +vk::PhysicalDevicePresentBarrierFeaturesNV barrierFeatures{}; +vk::PhysicalDeviceFeatures2 features2{ &barrierFeatures }; +physicalDevice.getFeatures2(&features2); + +if (barrierFeatures.presentBarrier) { + // When creating the swapchain, we join Barrier 1 + // This barrier is managed by the physical sync cable between machines + vk::SwapchainPresentBarrierCreateInfoNV barrierInfo{}; + barrierInfo.presentBarrierEnable = VK_TRUE; + barrierInfo.barrierID = 1; + + vk::SwapchainCreateInfoKHR createInfo{}; + createInfo.pNext = &barrierInfo; + // ... +} +---- + +== Genlock vs. Swaplock + +* **Genlock (Generator Lock)**: Syncs the *beginning* of the video signal (the start of the scanline). This prevents "rolling" artifacts. +* **Swaplock (Swap Barrier)**: Syncs the *moment* the front and back buffers are swapped. This prevents "stale frame" artifacts. + +By combining Genlock (signal timing) with Swap Barriers (frame timing), we achieve the "Gold Standard" of spatial synchronization: **Frame-Perfect Cluster Rendering**. The entire room updates as if it were a single piece of digital paper. + +xref:02_projector_based_spatial_tech.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..0c395ad32 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc @@ -0,0 +1,50 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Scaling our `simple_game_engine` to support **CAVE Architecture** and **Multi-GPU Synchronization** requires a shift to hardware-level barriers in `renderer_rendering.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Joining the Swap Barrier + +In `renderer_rendering.cpp`, we must update our `createSwapChain` logic to support the `VK_NV_present_barrier` extension for synchronized cluster rendering. + +[source,cpp] +---- +// renderer_rendering.cpp +bool Renderer::createSwapChain() { + // ... + vk::SwapchainPresentBarrierCreateInfoNV barrierInfo{ + .presentBarrierEnable = vk::True, + .presentBarrierName = 1 // All nodes in the CAVE must use the same ID + }; + + if (hasPresentBarrierSupport) { + createInfo.pNext = &barrierInfo; + } + + // Create the synchronized swapchain + swapChain = vk::raii::SwapchainKHR(device, createInfo); + // ... +} +---- + +== Synchronized Present Call + +In our `Render` function, the `presentKHR` call now becomes a synchronized event. The GPU will automatically block until all other nodes in the swap group are ready to flip their buffers. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... record and submit ... + + // This call now blocks at the hardware level if a Swap Barrier is active + // ensuring no tearing or "seams" between CAVE walls + presentQueue.presentKHR(presentInfo); +} +---- + +== Why These Changes? + +Hardware synchronization is the only way to ensure that the corners of a CAVE room look seamless. Without the `VK_NV_present_barrier`, the "seam" between projectors will always be visible as a line of tearing or stutter. By integrating this into our `createSwapChain` and `Render` logic, we transform the engine into a professional-grade simulation platform capable of driving multi-projector environments. + +xref:03_hardware_sync.adoc[Previous] | xref:../13_Warp_and_Blend/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc new file mode 100644 index 000000000..8cc388802 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc @@ -0,0 +1,28 @@ +:pp: {plus}{plus} += Warp and Blend using Vulkan Compute: Introduction + +In an ideal world, every projection surface in a **CAVE** would be perfectly flat, every projector would be perfectly aligned, and the walls would have zero thickness. In the real world, walls have slight curves, projectors are mounted at awkward angles, and corners are never truly 90 degrees. + +To solve these physical imperfections, we use **Warp and Blend** techniques. **Warping** is the process of geometrically distorting the rendered image so that it looks correct when projected onto a non-flat surface. **Blending** (or Edge Blending) is the process of smoothing the brightness in areas where multiple projectors overlap, preventing "hot spots" of double-brightness. + +In this chapter, we will explore how to use the power of **Vulkan Compute** to handle these post-processing tasks with maximum efficiency: + +1. **Geometric Correction**: Using compute shaders to map our 3D rendered scenes onto curved or irregular physical surfaces via a lookup table (**LUT**). +2. **Edge Blending**: Implementing alpha-blending logic in compute to seamlessly join multiple projector outputs. +3. **Lens Distortion Correction**: Utilizing Slang to author high-performance warping shaders that can be shared across both CAVE systems and HMDs. + +== Why Use Compute? + +While warping could be done in a traditional fragment shader by rendering a screen-aligned quad, **Vulkan Compute** offers several advantages for this specific task: + +* **Atomic Precision**: Compute shaders allow us to use atomic operations if we need to build complex histograms for auto-calibration. +* **Shared Memory (LDS)**: We can use **groupshared** memory to perform high-quality filtering (like bicubic interpolation) during the warp, which is significantly higher quality than standard hardware bilinear filtering. +* **Asynchronous Execution**: As we learned in the *Advanced Vulkan Compute* tutorial, we can run these warping kernels on an **Async Compute Queue** while the next frame is already being rendered on the graphics queue. + +== The Calibration Pipeline + +Warping is only as good as the data driving it. Most professional installations use a camera-based calibration system that generates a **Warp Map**—a high-precision texture where each pixel contains the (u, v) coordinates of where that pixel should actually "land" on the physical wall. + +By the end of this chapter, you will be able to take a standard rectilinear render and "warp" it into any physical shape required by the installation, ensuring that the virtual world remains perfectly undistorted to the user's eyes. + +xref:../12_CAVE_Architecture/04_incorporating_into_the_engine.adoc[Previous] | xref:02_geometric_correction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc new file mode 100644 index 000000000..82eae6e06 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc @@ -0,0 +1,61 @@ +:pp: {plus}{plus} += Geometric Correction: Mapping 3D Scenes to Physical Surfaces + +The core of **Geometric Correction** is a coordinate transformation. We start with a perfectly rectangular image rendered by our engine (the "Source"), and we need to transform it into a "Warped" image that looks correct when projected onto a physical surface like a curved screen or a corner. + +== The Concept: Reverse Lookup + +To understand warping, you must think in reverse. Instead of asking "Where does this source pixel go?", we ask **"For this pixel on my wall, where should I look in my rendered frame?"** + +1. **Rectilinear Source**: A standard 3D render (straight lines, 90-degree corners). +2. **Physical Destination**: A curved screen or a corner where two walls meet. +3. **The Warp Map**: A Look-Up Table (LUT) that maps every destination pixel to a $(u, v)$ coordinate in the source render. + +Treating correction as a reverse lookup is essential because it ensures that every pixel on the output display is filled exactly once, preventing gaps or "holes" in the final image. + +== Implementing the Warp Map + +The "brain" of this operation is the **Warp Map**. This is typically a `vk::raii::Image` containing floating-point data. + +[source,slang] +---- +// A simple compute-based warping kernel in Slang +RWTexture2D outputTarget; // The final image sent to the projector +Texture2D sourceRender; // The rectilinear frame we rendered +Texture2D warpMap; // The LUT containing (u, v) coordinates + +[numthreads(16, 16, 1)] +void main(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + float2 outputDim; + outputTarget.GetDimensions(outputDim.x, outputDim.y); + + if (dispatchThreadID.x >= outputDim.x || dispatchThreadID.y >= outputDim.y) + return; + + // 1. Look up the source coordinate from the Warp Map + // This tells us: "To draw pixel (x,y) on the wall, look at (u,v) in the render" + float2 sourceUV = warpMap.Load(int3(dispatchThreadID.xy, 0)); + + // 2. Sample the source image using high-quality interpolation + // We use Bilinear or Bicubic filtering to prevent "aliasing" + SamplerState linearSampler; + float4 color = sourceRender.SampleLevel(linearSampler, sourceUV, 0); + + // 3. Write the warped pixel to the output + outputTarget[dispatchThreadID.xy] = color; +} +---- + +== Luminance Summation and Edge Blending + +In a multi-projector setup, the edges of the images overlap. + +* **The Problem**: In the overlap region, you have light from *two* projectors hitting the same spot. This area will be twice as bright as the rest of the wall, creating a distracting "hot spot." +* **The Solution**: **Luminance Edge Blending**. We include an alpha channel in our Warp Map that defines a smooth gradient (1.0 to 0.0) in the overlap region. By multiplying the final color by this alpha, we "fade out" one projector as the other "fades in," maintaining uniform brightness across the entire wall. + +== Precision: Bicubic vs Sinc Filtering + +Because warping involves sampling a texture at non-integer coordinates, the quality of your filtering is paramount. While `SamplerState` with linear filtering is fast, it can lead to "blurring" in areas of high warp. High-end spatial engines often implement custom **Bicubic** or **Lanczos** filters in the compute shader to preserve sharpness during the coordinate transformation. + +xref:01_introduction.adoc[Previous] | xref:03_post_process_warping.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc new file mode 100644 index 000000000..055f547be --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc @@ -0,0 +1,56 @@ +:pp: {plus}{plus} += Post-Process Warping and Lens Distortion + +While the previous section focused on external physical surfaces like CAVE walls, **Warping** is also a fundamental part of head-mounted displays (HMDs). In a headset, the lenses themselves introduce significant **Pin-Cushion Distortion**. To counteract this, the engine must apply a **Barrel Distortion** post-process so that the final image looks rectilinear to the user. + +== The Physics of the Lens: Why Warp? + +To understand HMD warping, you must look at the glass. VR lenses are designed to take a tiny display and magnify it to cover your entire field of view. This magnification is not uniform. + +1. **Pin-Cushion Distortion**: The lens physically stretches the image more at the corners than in the center. +2. **The Solution**: We pre-distort the image in the opposite direction (**Barrel Distortion**). By "squeezing" the corners in our shader, the lens's physical stretch pulls them back into their correct rectilinear positions. + +== Mathematical Models of Distortion + +Most lens distortion can be modeled using a radial polynomial (like the **Brown-Conrady** model). This model uses coefficients ($k_1, k_2, k_3$) to shift the $(u, v)$ coordinates based on their distance from the optical center. + +[source,slang] +---- +// Applying a simple radial distortion model in Slang +float2 ApplyDistortion(float2 uv, float2 k) +{ + float2 center = float2(0.5, 0.5); + float2 d = uv - center; + float r2 = dot(d, d); + + // Barrel distortion: k[0] and k[1] are typically positive + // This shifts pixels towards the center based on their distance + float2 distortedD = d * (1.0 + k.x * r2 + k.y * r2 * r2); + + return center + distortedD; +} +---- + +== Chromatic Aberration: The Rainbow Effect + +Lenses also refract different wavelengths of light at different angles. This causes Red, Green, and Blue light to hit the user's eye at slightly different positions, creating "color fringing" or **Chromatic Aberration**. + +To fix this, we apply a slightly different warp to each color channel. In our Vulkan compute shader, this is as simple as performing three samples instead of one: + +[source,slang] +---- +float4 color; +// We sample the source image three times with different distortion coefficients +color.r = sourceRender.SampleLevel(s, ApplyDistortion(uv, k_red), 0).r; +color.g = sourceRender.SampleLevel(s, ApplyDistortion(uv, k_green), 0).g; +color.b = sourceRender.SampleLevel(s, ApplyDistortion(uv, k_blue), 0).b; +color.a = 1.0; +---- + +== Rendering 1.2x: The Oversampling Tax + +Because warping "squeezes" the image, some areas of your render will be scaled down while others are scaled up. To prevent the center of your view from looking blurry after the warp, you must render your 3D scene at a **Higher Resolution** than the physical display (typically 1.2x to 1.4x the panel resolution). This ensures that even after the distortion is applied, you still have 1:1 pixel mapping in the most critical areas. + +In the next chapter, we will move from pixels to 4D functions by exploring **LightField Theory**. + +xref:02_geometric_correction.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..7b48237e1 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc @@ -0,0 +1,74 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +The addition of **Warp and Blend** capabilities allows our `simple_game_engine` to handle geometric distortion and multi-projector blending in `renderer_rendering.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Enhancing the Post-Processing Chain + +In `renderer_rendering.cpp`, we introduce a compute-based "Warp" pass that runs after the main scene rendering is complete. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::Render(...) { + // ... Main Scene Pass (Raster) ... + + // NEW: Post-Process Warp Pass (Compute) + // This kernel maps our rectilinear render target to a warped swapchain image + auto& cmd = commandBuffers[currentFrame]; + + cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *warpPipeline); + + // Bind the Rectilinear Input (Descriptor Set 0) + cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *warpPipelineLayout, 0, *rectilinearSets[currentFrame], nullptr); + + // Bind the Warp Map LUT (Descriptor Set 1) + cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *warpPipelineLayout, 1, *warpMapSets, nullptr); + + // Dispatch the warp compute kernel + uint32_t groupCountX = (swapChainExtent.width + 15) / 16; + uint32_t groupCountY = (swapChainExtent.height + 15) / 16; + cmd.dispatch(groupCountX, groupCountY, 1); +} +---- + +== Slang Warp Shader Integration + +In our `warp.slang` shader, we use the Warp Map LUT to calculate the distorted texture coordinates. + +[source,slang] +---- +// warp.slang +Texture2D inputTexture : register(t0); +Texture2D warpMap : register(t1); // The LUT +RWTexture2D outputSwapchain : register(u0); + +[shader("compute")] +[numthreads(16, 16, 1)] +void computeMain(uint3 threadID : SV_DispatchThreadID) { + // 1. Sample the Warp Map to get the distorted coordinate + float2 distortedUV = warpMap.SampleLevel(sampler, float2(threadID.xy) / screenRes, 0); + + // 2. Sample the main scene render target + float3 color = inputTexture.SampleLevel(sampler, distortedUV, 0); + + // 3. Apply Edge Blending (alpha ramp) if needed + color *= calculateEdgeBlend(threadID.xy); + + outputSwapchain[threadID.xy] = float4(color, 1.0); +} + +// Edge Blending Helper: Multi-projector luminosity matching +float3 calculateEdgeBlend(uint2 pos) { + // Simple 10% overlap ramp for the left-edge of a projector + float overlapWidth = screenRes.x * 0.1f; + float alpha = clamp(float(pos.x) / overlapWidth, 0.0, 1.0); + return float3(alpha, alpha, alpha); +} +---- + +== Why These Changes? + +Proper warping and blending are what make a CAVE room feel like a unified environment. By handling distortion in the engine's compute-based post-processing chain, we can support a wide variety of hardware—from curved screens to custom headsets—without changing our core rasterization logic. Because our engine already has a modular `Renderer`, adding this compute pass is a natural extension of our existing frame loop. + +xref:03_post_process_warping.adoc[Previous] | xref:../14_LightField_Theory/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc new file mode 100644 index 000000000..895db3f97 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc @@ -0,0 +1,12 @@ +:pp: {plus}{plus} += Storing the Plenoptic Function + +To truly understand the future of spatial computing, we have to move beyond the idea of a simple "camera" looking at a "scene." In traditional rendering, we calculate the color of a pixel based on a single ray from the eye through a point on the image plane. But what if we could capture *all* the light flowing through a volume of space? This is the core of **Plenoptic** theory—from the Latin *plenus* (full) and the Greek *optikos* (of or relating to sight). + +At its most fundamental level, the **Plenoptic Function** is a seven-dimensional (7D) description of every possible light ray in the universe. It describes the intensity of light at any position latexmath:[x, y, z], from any direction latexmath:[\theta, \phi], at any wavelength latexmath:[\lambda], at any point in time latexmath:[t]. While this is a beautiful mathematical construct, rendering or storing a 7D function is computationally impossible for real-time systems. + +In this chapter, we are going to explore how we can simplify this monster into a manageable four-dimensional (4D) representation that we can actually store in Vulkan buffers. By assuming that light travels in straight lines through free space (the "Free Space Assumption"), and focusing on a static moment in time with a fixed set of colors, we can reduce our 7D function into a **4D LightField**. + +This 4D LightField is the "Holy Grail" for high-end spatial displays. It allows us to render holographic images where your eyes can naturally focus at different depths (solving the Vergence-Accommodation Conflict, or **VAC**) and see different perspectives as you move your head, all without the need for expensive per-eye re-rendering. We will look at how to define these rays using the classic "Two-Plane Parametrization"—the **ST plane** (spatial position) and the **UV plane** (directional orientation)—and how to map this mathematical grid into a high-performance memory layout for the engine. + +xref:../13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Previous] | xref:02_4d_lightfield_representation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc new file mode 100644 index 000000000..06240d387 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc @@ -0,0 +1,65 @@ +:pp: {plus}{plus} += 4D LightField Representation + +When we talk about representing a lightfield in the engine, we need a mathematical structure that maps a 4D light ray into a linear memory address. + +== The 7D Plenoptic Function: The Starting Point + +To understand lightfields, we must first look at the **Plenoptic Function** (from the Latin *plenus* meaning full and *opticus* meaning vision). This is a 7D function latexmath:[P(x, y, z, \theta, \phi, \lambda, t)] that describes every photon in the universe: + +* latexmath:[(x, y, z)]: Position in space. +* latexmath:[(\theta, \phi)]: Direction of travel. +* latexmath:[\lambda]: Wavelength (color). +* latexmath:[t]: Time. + +== Reduction to 4D: The Light Box + +Storing 7 dimensions is impossible. To make this practical for a GPU, we make three assumptions: + +1. **Static Scene**: We ignore time (latexmath:[t]). +2. **RGB Color**: We reduce wavelength (latexmath:[\lambda]) to three channels. +3. **Empty Space**: We assume light doesn't change color as it travels through air. This means we only need to know where a ray enters and leaves a "Light Box." + +This leaves us with **4 Dimensions**: two for the entry point (**ST plane**) and two for the entry direction (**UV plane**). This is the **Two-Plane Parametrization**. + +== Mapping to Vulkan Memory + +In Vulkan, we represent this 4D structure as a massive `vk::raii::Buffer`. Because the dataset is so large, we use `RWStructuredBuffer` in Slang to handle the indexing. + +[source,cpp] +---- +// Initializing a 4D LightField buffer using RAII +vk::BufferCreateInfo bufferInfo{ + .size = spatialRes * directionalRes * sizeof(glm::vec4), + .usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + .sharingMode = vk::SharingMode::eExclusive +}; + +vk::raii::Buffer lightFieldBuffer(device, bufferInfo); +---- + +== Indexing the 4th Dimension + +In our Slang shaders, we treat this linear memory as a 4D structure. Slang's ability to handle custom indexing makes it easy to calculate the 1D index from our 4D coordinates. + +[source,slang] +---- +struct LightFieldRay { + float4 color; +}; + +RWStructuredBuffer lightFieldData; + +// Mapping 4D coordinates to a linear index +// st: entry point on the spatial plane +// uv: entry point on the directional plane +uint getLinearIndex(uint2 st, uint2 uv, uint2 stRes, uint2 uvRes) { + return st.x + st.y * stRes.x + + uv.x * (stRes.x * stRes.y) + + uv.y * (stRes.x * stRes.y * uvRes.x); +} +---- + +By using this representation, the engine can treat a lightfield as a simple array of "light fragments," making it compatible with existing spatial pipelines while providing the foundation for complex plenoptic synthesis. + +xref:01_introduction.adoc[Previous] | xref:03_high_density_view_arrays.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc new file mode 100644 index 000000000..56aac19c0 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc @@ -0,0 +1,61 @@ +:pp: {plus}{plus} += High-Density View Arrays + +Managing a 4D lightfield poses a significant memory challenge. Even at modest resolutions, the total number of color samples can quickly reach gigabytes. + +== The Gigabyte Problem + +Consider a modest lightfield: +* **Spatial**: latexmath:[1024 \times 1024] entry points. +* **Directional**: latexmath:[32 \times 32] angles. +* **Data**: latexmath:[1024 \times 1024 \times 32 \times 32 \approx 1 \text{ billion rays}]. + +At 16 bytes per ray (RGBA FP32), that is **16 GB of VRAM**. This exceeds the capacity of many consumer GPUs. To make this work, we must optimize both storage and access. + +== FP16 and Quantization: Bandwidth Savings + +The first step is reducing the precision of each ray. Because lightfields often exhibit high directional redundancy (adjacent rays are very similar), we can compress the color data. + +In Slang, we can use `half4` instead of `float4`. This immediately halves the VRAM footprint to **8 GB**. + +== Buffer Device Address (BDA): Pointer-Based Access + +With such large datasets, the overhead of standard Vulkan Descriptor Sets can become a bottleneck. By using **Buffer Device Address**, we can pass a raw 64-bit pointer to our lightfield data directly into our shaders. + +[source,cpp] +---- +// Using BDA to get a raw pointer to a high-density buffer +vk::BufferDeviceAddressInfo addressInfo{ + .buffer = *lightFieldBuffer +}; + +uint64_t bufferPointer = device.getBufferAddress(addressInfo); +---- + +This is particularly useful for **N-View** systems where we might be accessing dozens of different view layers in a single pass. We can simply pass the pointer once and calculate offsets mathematically. + +== Cache Locality: Spatial-Directional Tiling + +A standard 4D array layout is terrible for GPU caches. If the shader samples a ray and then its neighbor, the neighbor might be megabytes away in linear memory, causing a "Cache Miss." + +To fix this, we implement **4D Tiling**. We reorganize the data so that a latexmath:[4 \times 4 \times 4 \times 4] block of spatial and directional rays are stored contiguously. This ensures that when a Compute Unit (CU) fetches one sample, the neighboring rays are already in the L1/L2 cache. + +[source,slang] +---- +// Tiled 4D indexing example in Slang +uint getTiledIndex(uint2 st, uint2 uv) { + uint2 tileSize = uint2(8, 8); + uint2 tileCoord = st / tileSize; + uint2 localCoord = st % tileSize; + + // Reorganizing memory to keep spatial neighbors together + return (tileCoord.x + tileCoord.y * stResTiles) * (tileSize.x * tileSize.y) + + (localCoord.x + localCoord.y * tileSize.x); +} +---- + +By combining data compression, raw pointer flexibility via BDA, and tiled memory layouts, we can manage massive lightfield datasets that would otherwise be impossible to store. + +In the next chapter, we will see how to turn these rays back into images using **Plenoptic Synthesis**. + +xref:02_4d_lightfield_representation.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..16bd50665 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc @@ -0,0 +1,61 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +The introduction of **LightField Theory** and plenoptic data structures requires our `simple_game_engine` to handle high-density memory arrays using **Buffer Device Address (BDA)** in `renderer_core.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Enabling BDA in the Backend + +In `renderer_core.cpp`, we must enable the BDA feature during logical device creation. This allows us to treat GPU buffers as raw 64-bit pointers in our Slang shaders. + +[source,cpp] +---- +// renderer_core.cpp +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + // ... + vk::PhysicalDeviceVulkan12Features vulkan12Features; + vulkan12Features.bufferDeviceAddress = vk::True; + + // NEW: Also enable 1.4 features for easier memory management + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.pNext = &vulkan12Features; + // ... +} +---- + +== Mapping 4D LightField Buffers + +In `renderer_resources.cpp`, we create a high-density buffer for our ST/UV planes. Instead of a standard descriptor set, we retrieve the raw device address. + +[source,cpp] +---- +// renderer_resources.cpp +// Supporting Struct: Push Constants for BDA +struct LightFieldPushConstants { + uint64_t lightFieldDataAddress; + float4 uvScaling; + // ... +}; + +void Renderer::createLightFieldBuffer(size_t totalBytes) { + // 1. Create the buffer with BDA usage + vk::BufferCreateInfo createInfo{ + .size = totalBytes, + .usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress + }; + lightFieldBuffer = vk::raii::Buffer(device, createInfo); + + // 2. Retrieve the raw 64-bit address + vk::BufferDeviceAddressInfo addressInfo{ .buffer = *lightFieldBuffer }; + uint64_t gpuPointer = device.getBufferAddress(addressInfo); + + // 3. Pass this pointer to our Slang shader via Push Constants + pushConstants.lightFieldDataAddress = gpuPointer; +} +---- + +== Why These Changes? + +By moving from traditional descriptor sets to BDA for lightfield data, we avoid the overhead of binding thousands of individual view textures. Our engine can now manage massive plenoptic datasets (often multi-gigabyte) as a single contiguous block of memory. This ensures maximum cache locality during the synthesis pass, which is critical for maintaining spatial immersion. + +xref:03_high_density_view_arrays.adoc[Previous] | xref:../15_Plenoptic_Synthesis/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc new file mode 100644 index 000000000..ecd9b8081 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc @@ -0,0 +1,12 @@ +:pp: {plus}{plus} += Generating Views from Sparse Data + +In the previous chapter, we looked at how to represent and store a 4D lightfield in Vulkan. But a static buffer of rays is only useful if we can reconstruct a perspective from it for the viewer's current eye position. This process—generating a new image from a discrete set of samples—is what we call **Plenoptic Synthesis**. + +The challenge of synthesis is that we will rarely have a direct ray that exactly matches the one we need for a given pixel. Instead, we have to perform high-speed interpolation between our sparse samples. This is essentially a specialized form of **Image-Based Rendering (IBR)**, where we are warping and blending existing data rather than rasterizing 3D geometry from scratch. + +In this chapter, we will explore the two primary paths for plenoptic synthesis: the **Raster path** and the **Ray Traced path**. The raster path uses specialized fragment shaders to quickly blend samples together, making it ideal for low-power mobile headsets. The ray traced path, on the other hand, utilizes Vulkan's hardware acceleration to trace primary rays directly through virtual microlens arrays, providing much higher fidelity and more complex optical effects. + +We'll look at how the engine coordinates these two paths using compute and fragment shaders and how to use Slang's productivity features to author complex synthesis logic without getting lost in the 4D math. + +xref:../14_LightField_Theory/04_incorporating_into_the_engine.adoc[Previous] | xref:02_synthesis_shaders.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc new file mode 100644 index 000000000..9af23bfe1 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc @@ -0,0 +1,50 @@ +:pp: {plus}{plus} += Specialized Fragment Shaders for Synthesis + +To turn a 4D lightfield back into a 2D image, we use a process called **Image-Based Rendering (IBR)**. Instead of drawing triangles, we treat our 4D buffer as a dense "cloud" of light rays and selectively pull the ones that would pass through our virtual camera lens. + +== The Concept: Reconstructing the Wavefront + +To understand synthesis, imagine your virtual camera is looking into the "Light Box" we defined in the previous chapter. + +1. **Ray Tracing**: For every pixel on your screen, you calculate a ray starting at your eye and passing through that pixel. +2. **Intersection**: You find where that ray hits the **ST plane** (position) and the **UV plane** (direction) of your lightfield. +3. **Synthesis**: You look up the color of the ray at those coordinates in your 4D buffer. + +== Quadrilinear Interpolation: The 16-Sample Lookup + +Because our lightfield is stored as a discrete grid, the ray we calculate will rarely hit an exact sample point. We must interpolate. In 4D, this requires **Quadrilinear Interpolation**. + +* **Bilinear on ST**: Interpolate between the 4 nearest spatial neighbors. +* **Bilinear on UV**: Interpolate between the 4 nearest directional neighbors. +* **Result**: To get a single smooth pixel, you must sample **16 different rays** (4 spatial $\times$ 4 directional). + +[source,slang] +---- +// Quadrilinear interpolation logic in Slang +float4 sampleLightField(float2 st, float2 uv) { + // 1. Calculate floor and fractional parts for both planes + uint2 st0 = uint2(floor(st)); + uint2 uv0 = uint2(floor(uv)); + float2 stF = frac(st); + float2 uvF = frac(uv); + + // 2. Perform ST-plane interpolation at the surrounding UV coordinates + // fetch() uses the linear index calculation we learned in the last chapter + float4 c00 = lerp(lerp(fetch(st0, uv0), fetch(st0 + uint2(1, 0), uv0), stF.x), + lerp(fetch(st0, uint2(0, 1), uv0), fetch(st0 + uint2(1, 1), uv0), stF.x), + stF.y); + // ... repeat for the other 3 UV neighbors (c10, c01, c11) ... + + // 3. Perform the final UV-plane interpolation + return lerp(lerp(c00, c10, uvF.x), lerp(c01, c11, uvF.x), uvF.y); +} +---- + +== Performance: Coalescing the Rays + +Because we are doing 16 lookups per pixel, we must be extremely mindful of **SIMT Coalescing**. If we are not careful with our memory layout, these 16 lookups will scatter across different DRAM pages, causing a "Memory Wall" where the GPU sits idle while waiting for data. + +By using the **4D Tiling** we implemented in Chapter 14, we ensure that these 16 samples are geographically close in physical VRAM. This allows the GPU to satisfy all 16 requests with just one or two cache line fetches, keeping our plenoptic synthesis running at high frame rates. + +xref:01_introduction.adoc[Previous] | xref:03_ray_traced_synthesis.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc new file mode 100644 index 000000000..6445b6528 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc @@ -0,0 +1,49 @@ +:pp: {plus}{plus} += Ray Traced Synthesis and Microlens Arrays + +While the raster path for plenoptic synthesis is fast, it's often an approximation. For high-fidelity holographic displays, we want to simulate the exact physical optics of a display—particularly how rays travel through a **Microlens Array** (MLA). This is where **Vulkan Ray Tracing** (VRT) becomes the engine's primary tool for synthesis. + +== Physical Optics: The Microlens Array (MLA) + +To understand ray-traced synthesis, you must understand how lightfield cameras work. They use a **Microlens Array**—a grid of thousands of tiny lenses placed in front of the sensor. Each microlens captures a small "patch" of the scene from multiple angles. + +In our ray tracing pipeline, we reverse this process: + +1. **Primary Ray**: We trace a ray from the virtual eye to the screen. +2. **Microlens Intersection**: We simulate the ray passing through a virtual microlens. +3. **Refraction**: We calculate how the lens bends the ray based on its physical properties (focal length, curvature). +4. **LightField Lookup**: The refracted ray now points to a specific 4D coordinate in our plenoptic buffer. + +== Implementing the Virtual Lens in Slang + +Using the `RaytracingAccelerationStructure` in Vulkan, we can simulate complex lenses that a raster shader cannot. + +[source,slang] +---- +// RayGen shader in Slang for plenoptic synthesis +[shader("raygeneration")] +void rayGenMain() { + uint2 pixelID = DispatchRaysIndex().xy; + float2 uv = float2(pixelID) / float2(DispatchRaysDimensions().xy); + + // 1. Trace a ray through the virtual microlens optics + // This function calculates the refraction based on the MLA geometry + RayDesc ray = generateRayThroughLens(uv); + + // 2. Intersect the ray with our 4D LightField "box" (the ST/UV planes) + float4 color = intersectLightField(ray, lightFieldData); + + // 3. Write the final result to the OpenXR swapchain image + outputImage[pixelID] = color; +} +---- + +== Why Ray Trace? Physical Fidelity + +Ray tracing allows us to simulate **Vergence-Accommodation Conflict (VAC)** solutions. By adjusting the virtual focal length of our microlens array in real-time, we can change the "Plane of Focus" of the holographic image. This allows the user's eyes to naturally focus on near or far virtual objects, eliminating the eye strain common in standard fixed-focus headsets. + +Furthermore, ray tracing allows us to handle **Non-Uniform LightFields**. If our 4D data is stored in a sparse tree (like the Octrees we covered in the Compute tutorial), ray tracing can traverse those structures much more efficiently than a series of nested loops in a fragment shader. + +By combining the 4D theory of LightFields with the physical accuracy of Vulkan Ray Tracing, we create a spatial rendering pipeline that can handle the most advanced holographic displays on the market. + +xref:02_synthesis_shaders.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..910310fa2 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc @@ -0,0 +1,75 @@ +:pp: {plus}{plus} += Incorporating into the Engine + +Integrating **Plenoptic Synthesis** into our `simple_game_engine` requires a flexible rendering architecture that can switch between traditional rasterization and advanced ray tracing in `renderer_rendering.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Implementing the Raster Synthesis Path + +In `renderer_rendering.cpp`, we treat raster synthesis as a full-screen fragment pass. We sample the lightfield buffer using the quadrilinear interpolation logic we built. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::RenderSynthesisRaster(...) { + // ... + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *rasterSynthesisPipeline); + + // Bind the LightField buffer (using BDA address from push constants) + cmd.pushConstants(*pipelineLayout, + vk::ShaderStageFlagBits::eFragment, 0, synthesisPushConstants); + + // Draw full-screen triangle to trigger fragment synthesis + cmd.draw(3, 1, 0, 0); +} +---- + +== Implementing the Ray Traced Path + +If the engine detects `VK_KHR_ray_tracing_pipeline` support, we can use the more accurate Ray Traced synthesis path. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::RenderSynthesisRayTraced(...) { + // ... + cmd.bindPipeline(vk::PipelineBindPoint::eRayTracingKHR, *rtSynthesisPipeline); + + // Dispatch rays through our virtual microlens array + // One ray per pixel of the high-res spatial display + cmd.traceRaysKHR(sbtRayGen, sbtMiss, sbtHit, sbtCallable, + swapChainExtent.width, swapChainExtent.height, 1); +} +---- + +== Sharing Logic via Slang Modules + +We use Slang to ensure that the core interpolation logic is identical across both the raster and ray traced paths. + +[source,slang] +---- +// synthesis_utils.slang +// Common logic for both paths +float3 samplePlenopticFunction(uint64_t bufferPtr, float4 ray) { + // Shared quadrilinear interpolation math + // ... +} + +// fragment_synthesis.slang +[shader("pixel")] +float4 fragmentMain(V2P input) : SV_Target { + return float4(samplePlenopticFunction(pushConstants.ptr, input.ray), 1.0); +} + +// raygen_synthesis.slang +[shader("raygeneration")] +void raygenMain() { + float4 ray = calculateRayFromMicrolens(DispatchRaysIndex().xy); + outputImage[DispatchRaysIndex().xy] = samplePlenopticFunction(pushConstants.ptr, ray); +} +---- + +== Why These Changes? + +By providing dual paths in our `Renderer`, we allow the engine to scale from low-power mobile AR glasses (using the raster path) to high-end holographic workstation displays (using ray traced paths). Because our engine already has a modular `Render` structure and utilizes Slang, sharing the complex 4D interpolation logic between these paths ensures mathematical consistency and visual fidelity. + +xref:03_ray_traced_synthesis.adoc[Previous] | xref:../16_Scene_Understanding/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc new file mode 100644 index 000000000..3b0a0a252 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc @@ -0,0 +1,10 @@ +:pp: {plus}{plus} += Scene Understanding: Data Ingestion and Semantic Labels + +In previous chapters, we focused on rendering virtual objects into the real world. But true spatial computing is a two-way street. To create convincing interactions—like virtual balls bouncing off your actual coffee table or virtual characters hiding behind your real couch—our engine needs to "see" and "understand" the physical environment. + +This is the domain of **Scene Understanding**. In this chapter, we will explore how OpenXR runtimes use sensors like **LiDAR** (Light Detection and Ranging) and depth cameras to build a dynamic 3D model of the user's room. We'll learn how to ingest this data as **Spatial Meshes**, how to use **Semantic Labels** to distinguish a "Floor" from a "Human," and how to achieve ultra-low latency via **Zero-Copy Hand-off** directly into our Vulkan compute pipeline. + +By the end of this chapter, you'll understand how to turn the messy physical world into structured geometry that our engine can treat just like any other 3D asset. + +xref:../15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Previous] | xref:02_environmental_ingestion.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc new file mode 100644 index 000000000..3fa5619f4 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc @@ -0,0 +1,43 @@ +:pp: {plus}{plus} += Environmental Ingestion: Scanning the Digital Twin + +To make our virtual objects interact with the physical world, our engine must first capture that world's geometry. This process is known as **Environmental Ingestion** or **Scene Understanding**. Using OpenXR extensions like `XR_MSFT_scene_understanding` or the cross-vendor **Scene Mesh** functionality, we can bridge the gap between photons and triangles. + +== The Concept: The Digital Twin + +Scene understanding turns the messy physical world into a structured **Digital Twin**. The XR runtime uses the headset's cameras and depth sensors (like **LiDAR**) to build this twin in real-time. This isn't a static model; it is a dynamic, evolving representation of the user's environment. + +1. **Spatial Meshing**: The runtime generates a 3D triangle mesh of all visible surfaces in the room. This mesh is updated as the user moves and looks around. +2. **Semantic Labels**: The runtime categorizes parts of that mesh. Instead of just "triangles," the engine knows: "This is a Floor," "This is a Table," or "This is a Human." +3. **Plane Detection**: For simpler logic, the runtime identifies large flat surfaces, which are much cheaper to process than full 3D meshes for tasks like placing a virtual lamp. + +== Accessing Meshes in Vulkan + +When we request a scene mesh from OpenXR, we aren't just getting a one-time blob of data. We are subscribing to a stream of updates. In our engine, we handle this by creating dynamic `vk::raii::Buffer` objects that we update whenever the runtime signals that the environment has changed. + +Crucially, because this data is generated by the headset's sensors, we often receive it in specialized formats. OpenXR allows us to query these buffers directly, but we must be careful with our synchronization—using **Timeline Semaphores** to ensure the GPU doesn't try to render a spatial mesh while the XR runtime is still updating its vertices. + +[source,cpp] +---- +// Querying the scene mesh from OpenXR with vulkan-hpp +XrSceneMeshBuffersMSFT meshBuffers{XR_TYPE_SCENE_MESH_BUFFERS_MSFT}; +meshBuffers.vertexCapacityInput = maxVertices; +meshBuffers.vertexBuffer = reinterpret_cast(*vertexBufferMemory); +meshBuffers.indexCapacityInput = maxIndices; +meshBuffers.indexBuffer = reinterpret_cast(*indexBufferMemory); + +// The call that populates our Vulkan-mapped memory +xrGetSceneMeshBuffersMSFT(sceneHandle, meshId, &meshBuffers); +---- + +== Semantic Labels: Meaningful Interaction + +Semantic labels are the "Intelligence Layer" of scene understanding. Instead of just seeing geometry, our engine can ask: "Give me all surfaces labeled as **FLOOR**." This allows us to implement high-level spatial logic: + +* **Physics Interaction**: Virtual objects should bounce off tables but pass through "Air" (regions the sensor hasn't scanned yet). +* **AI Navigation**: Characters should only walk on surfaces labeled as "Floor." +* **Contextual Occlusion**: If a mesh is labeled as "Wall," we can prioritize it for occlusion tests, ensuring that virtual windows don't accidentally render "inside" your physical walls. + +By ingesting these meshes directly into our Vulkan buffers, we enable real-time interaction between our shaders and the user's physical room. + +xref:01_introduction.adoc[Previous] | xref:03_zero_copy_hand_off.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc new file mode 100644 index 000000000..75b6217bb --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc @@ -0,0 +1,45 @@ +:pp: {plus}{plus} += Zero-Copy Hand-off for Sensor Data + +To achieve real-time scene understanding, we cannot afford to process high-resolution sensor data on the CPU. Moving depth maps or raw camera feeds from the headset's hardware into system RAM, processing them with the CPU, and then sending them back to the GPU is simply too slow for Mixed Reality (**MR**). We need a **Zero-Copy Hand-off**. + +== The Concept: Direct Memory Access (DMA) + +In a zero-copy hand-off, the OpenXR runtime captures sensor data directly into a GPU buffer that our Vulkan engine can see. This uses **DMA** (Direct Memory Access) to bypass the host processor entirely. + +1. **VRAM Allocation**: The XR runtime or the OS driver allocates a region of **VRAM** (Video RAM) for the sensor data. +2. **External Memory Handle**: OpenXR provides a handle to this memory using extensions like `VK_KHR_external_memory`. +3. **Engine Import**: Our engine imports this handle, wrapping it in a `vk::raii::Image` or `vk::raii::Buffer`. +4. **Compute Access**: Our compute shaders (authored in **Slang**) process this raw data directly in-place. + +== Implementation: Piping Data to Compute + +By bypassing the CPU, we reduce latency and free up cycles for the main game simulation. In our engine, we use this for **Sensor Fusion**—the process of combining depth data with color camera feeds to create high-fidelity, textured spatial meshes. + +When importing these buffers, we must ensure our Vulkan memory types match the requirements of the external handle. This typically requires matching the **LUID** (Locally Unique Identifier) of the device that created the memory. + +[source,cpp] +---- +// Importing external sensor memory into Vulkan RAII +vk::ImportMemoryBufferInfoKHR importInfo{ + .handleType = vk::ExternalMemoryHandleTypeFlagBits::eOpaqueFd, // Linux/Android (or Win32 on Windows) + .handle = sensorMemoryHandle +}; + +vk::MemoryAllocateInfo allocInfo{ + .pNext = &importInfo, + .allocationSize = sensorDataSize, + .memoryTypeIndex = findSensorCompatibleMemoryType() +}; + +// RAII handle automatically manages the lifetime of the imported memory +vk::raii::DeviceMemory importedMemory(device, allocInfo); +---- + +== Why Zero-Copy is Critical for MR + +In Mixed Reality (MR), users are often viewing the real world through **Passthrough**—a digital video feed of their surroundings. If there is even a 20ms delay between the camera capturing the room and the GPU displaying it, the user will feel a nauseating disconnect between their physical movement and the visual feed. This is known as **Motion-to-Photon Latency**. + +Zero-copy hand-offs ensure that sensor data moves at the speed of the GPU's internal bus, keeping the latency below the threshold of human perception and maintaining the illusion that the virtual and physical worlds occupy the same space. + +xref:02_environmental_ingestion.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..d445ae558 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc @@ -0,0 +1,82 @@ +:pp: {plus}{plus} += Incorporating Scene Understanding into the Engine + +Integrating **Scene Understanding** allows our `simple_game_engine` to treat the physical room as dynamic geometry. We achieve this by bridging OpenXR spatial data with our engine's `Entity` and `MeshComponent` systems. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp]. + +== Tracking Spatial Mesh Lifecycle + +In `engine.cpp`, within the `Engine::update()` loop, we add a synchronization step that queries OpenXR for updated spatial meshes. This process ensures that virtual objects interact with the room as it is scanned. + +We create a dedicated method, `Engine::updateSpatialMeshes()`, which iterates through the runtime's mesh stream. + +[source,cpp] +---- +// engine.cpp +// Supporting Struct: Spatial Mesh Bridge +struct XrSpatialMesh { + XrGuidMSFT meshGuid; + std::vector vertices; + std::vector indices; + glm::mat4 transform; +}; + +void Engine::updateSpatialMeshes() { + if (!xrMode) return; + + // 1. Retrieve current spatial meshes from the OpenXR context + auto xrMeshes = xrContext.getLatestSpatialMeshes(); + + for (const auto& xrMesh : xrMeshes) { + // 2. Map the XR mesh ID to an Engine Entity + // We use a internal map: std::unordered_map + Entity* spatialEntity = getOrCreateSpatialEntity(xrMesh.meshGuid); + + auto* meshComp = spatialEntity->getComponent(); + auto* transform = spatialEntity->getComponent(); + + // 3. Update the GPU resources via our Renderer + // This transfers raw XrVector3f data into our vk::raii::Buffer + renderer->updateDynamicMesh( + meshComp->getBuffer(), + xrMesh.vertices, + xrMesh.indices + ); + + // 4. Update the transform to match the physical room coordinates + // OpenXR spatial data is already provided as a 4x4 matrix + transform->setLocalMatrix(xrMesh.transform); + } +} +---- + +== Applying Semantic Intelligence + +In `mesh_component.h`, we extend the component to store a **Semantic Label**. This allows our physics and AI systems to query whether a specific piece of geometry represents a "Floor," "Table," or "Wall." + +When the engine detects a new mesh, we assign its properties in `engine.cpp`: + +[source,cpp] +---- +// engine.cpp +void Engine::configureSpatialEntity(Entity* entity, XrSceneComponentTypeMSFT type) { + auto* meshComp = entity->getComponent(); + + if (type == XR_SCENE_COMPONENT_TYPE_PLANE_MSFT) { + // High-level plane: Perfect for floor-based AI navigation + meshComp->setSemanticTag("Floor"); + entity->addComponent(Physics::Static); + } else if (type == XR_SCENE_COMPONENT_TYPE_OBJECT_MSFT) { + // Physical object: Enable shadows and complex physics + meshComp->setSemanticTag("Prop"); + entity->addComponent(Physics::Kinematic); + } +} +---- + +== Architectural Win: Unified Pipelines + +By mapping spatial data into our existing `Entity` system, we don't need a "special" renderer for the real world. Our **PBR** (Physically Based Rendering) pipeline in `renderer_rendering.cpp` simply sees these spatial entities as standard geometry. They receive virtual light, cast virtual shadows, and provide depth information for our transparency passes automatically. + +This "Unified Geometry" approach is the foundation of believable Mixed Reality, allowing us to treat the physical room and our virtual assets as a single, coherent scene. + +xref:03_zero_copy_hand_off.adoc[Previous] | xref:../17_ML_Inference_Spatial/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc new file mode 100644 index 000000000..b5edd9895 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc @@ -0,0 +1,10 @@ +:pp: {plus}{plus} += ML Inference with Cooperative Matrices + +As we've seen in the previous chapter, the sensors on a modern XR headset provide a flood of raw physical data. But capturing triangles is only half the battle. To create truly intelligent spatial applications, we need to "understand" that data in real-time. + +Is that moving cluster of vertices a user's hand? Is it a cat jumping on the sofa? To answer these questions without stalling our rendering pipeline, we turn to **ML Inference** (Machine Learning). In this chapter, we'll explore how to leverage the **Cooperative Matrices** in Vulkan 1.4 to perform high-speed neural network calculations directly on the GPU. + +By moving inference from the CPU to the GPU's specialized matrix hardware (like **Tensor Cores** or **Matrix Accelerators**), we can achieve sub-millisecond latency for tasks like hand-pose refinement, semantic segmentation, and mesh reconstruction. We'll learn how to integrate these ML kernels into our engine's compute pipeline, turning noisy sensor data into clean, interactive inputs. + +xref:../16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Previous] | xref:02_on_gpu_inference.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc new file mode 100644 index 000000000..82b3bcba6 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc @@ -0,0 +1,54 @@ +:pp: {plus}{plus} += On-GPU Inference with Cooperative Matrices + +To process the flood of real-world data coming from our zero-copy sensor hand-offs, our engine needs a way to "understand" geometry and motion instantly. Standard compute shaders are excellent for general-purpose parallel tasks, but neural networks rely on heavy matrix multiplication—a task that standard **ALU** (Arithmetic Logic Unit) pipelines can find inefficient. + +To solve this, we use **Cooperative Matrices** (as detailed in our Advanced Compute series), which tap into the GPU's dedicated hardware for high-speed matrix math. + +== The Concept: Specialized ML Hardware + +In a spatial engine, we use ML (Machine Learning) for tasks like **Hand Tracking** and **Object Recognition**. These tasks involve multiplying massive matrices of weights against input sensor data to produce probabilities or coordinates. + +* **Standard Compute**: Each thread works independently on a single scalar or vector operation. To multiply a large matrix, threads must coordinate manually via shared memory, often hitting bandwidth bottlenecks. +* **Cooperative Matrices**: A whole **Subgroup** (32 or 64 threads) works as a single unit to multiply a large matrix tile in one operation. This "cooperation" allows the hardware to bypass traditional cache levels and use high-speed internal paths (like **Tensor Cores** on NVIDIA or **Matrix Accelerators** on AMD). + +== Implementing Hand-Pose Refinement + +Consider raw hand-tracking data from a headset. It is often noisy—your virtual fingers might "jitter" as the sensor loses sight of them. We can use a small neural network to **Refine** these poses, using the previous 5 frames of movement to predict the most likely "smooth" position of each joint. + +By performing this inference on the GPU, we can use **GEMM** (General Matrix Multiply) operations to process all joint refinements in parallel. + +[source,slang] +---- +// Using Slang's Matrix fragments for cooperative ML inference +// Note: Requires Vulkan 1.4 with VK_KHR_cooperative_matrix +[shader("compute")] +[numthreads(32, 1, 1)] // Subgroup-sized for optimal matrix math +void performHandRefinement() { + // 1. Load raw joint data and neural network weights into fragments + // These matrices represent a 16x16 tile of the larger network + cooperative_matrix input; + cooperative_matrix weights; + + input.load(rawJointBuffer, jointOffset); + weights.load(weightBuffer, weightOffset); + + // 2. Perform high-speed Matrix-Multiply-Accumulate (MMA) + // The hardware handles the subgroup-wide synchronization automatically + auto result = mul(input, weights); + + // 3. Store the "Refined" joint positions directly back to the Action Space buffer + result.store(refinedPoseBuffer, poseOffset); +} +---- + +== Why On-GPU? Latency and Power + +Performing ML inference on the GPU is critical for spatial computing for two primary reasons: + +1. **Latency**: If we sent sensor data to the CPU for ML processing, the finger movement would visibly lag behind the user's physical hand. This breaks the feeling of **Presence**. +2. **Power Efficiency**: Matrix accelerators are significantly more energy-efficient than general-purpose ALUs for these specific tasks. For mobile headsets, this translates directly to longer sessions and reduced thermal throttling. + +By integrating ML directly into our spatial compute pipeline, we turn raw, noisy sensor data into clean, interactive inputs with the same low-latency performance as our main rendering path. + +xref:01_introduction.adoc[Previous] | xref:03_refining_spatial_data.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc new file mode 100644 index 000000000..9282421cc --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc @@ -0,0 +1,51 @@ +:pp: {plus}{plus} += Refining Spatial Data: From Messy to Clean + +The environmental meshes we ingested in Chapter 16 are often "noisy." They contain holes where the LiDAR sensor couldn't reach, jagged edges on curved surfaces, and floating artifacts caused by reflections or moving objects. To turn these into clean, interactive geometry, we use ML-driven refinement. + +== The Concept: Topology Inference + +Standard mesh smoothing algorithms (like Laplacian or Gaussian smoothing) can remove noise, but they are "dumb"—they can't fill large holes or "guess" missing geometry. **Topology Inference** uses a neural network trained on millions of real-world scans to predict the missing parts of a scene. + +1. **Denoising**: The model identifies small floating vertices or "geometry dust" that doesn't belong to any major surface and marks them for removal. +2. **Infilling**: The model predicts the curvature of a surface (like a wall behind a plant) and generates new triangles to bridge gaps in the scan. +3. **Primitive Approximation**: For optimal performance, the model can suggest replacing a complex, high-poly "Table" mesh with a clean, low-poly box primitive that has the same physical bounds. + +== Implementation: ML-Aided Surface Reconstruction + +In our engine, we use a compute shader to feed the raw spatial vertices into our ML model (processed via cooperative matrices). The model outputs a **Saliency Map**—a probability grid that tells us which regions are likely solid, high-confidence surfaces and which are noise. + +We author this reconstruction logic in **Slang**, using structured buffers to manage our dynamic mesh data. + +[source,slang] +---- +// Using ML results to guide mesh reconstruction in Slang +[shader("compute")] +[numthreads(64, 1, 1)] +void reconstructSurfaces(uint3 dtid : SV_DispatchThreadID) { + // 1. Query the ML saliency map for this vertex's region + float surfaceProbability = mlSaliencyMap.Load(dtid.xy); + + if (surfaceProbability > 0.95) { + // 2. High confidence: Snap the vertex to the predicted "clean" plane + float3 refinedPos = predictCleanSurface(dtid.xyz); + updateMeshVertex(dtid.x, refinedPos); + } else if (surfaceProbability < 0.1) { + // 3. Low confidence: This is likely a sensor artifact (e.g., a mirror reflection) + // Mark the vertex as "degenerate" to prevent it from rendering + degenerateMeshVertex(dtid.x); + } +} +---- + +== Interactive Geometry: The Final Goal + +By refining the spatial data, we move from simple "Visual Passthrough" to true **Physical Interaction**. + +* **Pixel-Perfect Occlusion**: A clean, refined wall mesh allows for perfect depth-aware occlusion of virtual objects, preventing them from "leaking" through the edges of physical furniture. +* **Realistic Shadows**: We can cast virtual shadows onto a refined "Table" mesh. Because the mesh is clean, the shadows look crisp and "grounded," making virtual objects feel like they truly exist in the room. +* **Robust Physics**: A refined floor mesh prevents virtual objects from falling through small "cracks" in the raw spatial scan, ensuring a stable simulation. + +By using on-GPU ML to polish our spatial data, we bridge the final gap between the messy physical world and the digital precision required for high-end XR. In the next chapter, we will see how to combine these refined meshes with our rendering to achieve **Semantic Occlusion**. + +xref:02_on_gpu_inference.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..87b262689 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc @@ -0,0 +1,76 @@ +:pp: {plus}{plus} += Incorporating ML Inference into the Engine + +Integrating **ML Inference** allows our `simple_game_engine` to perform high-level spatial refinement. We implement this by adding a dedicated compute pass to our `Renderer` that utilizes **Cooperative Matrices** for maximum throughput. *This is currently left as an exercise for the reader to implement.* + +== Integrating the Inference Pass + +In `renderer_rendering.cpp`, we introduce a dedicated step within our `Renderer::render()` loop. This pass must occur after we've ingested the latest sensor data but before we use that data for occlusion or physics. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::executeSpatialInference(vk::raii::CommandBuffer& cmd, uint32_t frameIndex) { + if (!xrMode || !mlEnabled) return; + + // 1. Bind the specialized ML compute pipeline + // This pipeline is configured with VK_KHR_cooperative_matrix enabled + cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *mlInferencePipeline); + + // 2. Bind descriptors for weights, raw sensor data, and refined output + cmd.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + *mlPipelineLayout, + 0, + *mlDescriptorSets[frameIndex], + nullptr + ); + + // 3. Dispatch the ML kernel + // We dispatch in subgroup-sized tiles (e.g., 16x16 fragments) + uint32_t groupsX = (inferenceWidth + 15) / 16; + uint32_t groupsY = (inferenceHeight + 15) / 16; + cmd.dispatch(groupsX, groupsY, 1); + + // 4. Memory Barrier: Ensure ML results are visible to subsequent vertex/fragment stages + vk::MemoryBarrier2 refinementBarrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eComputeShader, + .srcAccessMask = vk::AccessFlagBits2::eShaderWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eVertexAttributeInput | + vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eAttributeRead | + vk::AccessFlagBits2::eShaderRead + }; + + vk::DependencyInfo dependencyInfo{ .memoryBarrierCount = 1, .pMemoryBarriers = &refinementBarrier }; + cmd.pipelineBarrier2(dependencyInfo); +} +---- + +== Managing ML Weights in the Engine + +Neural network weights are essentially large static arrays. In `resource_manager.cpp`, we treat these as a specialized asset type, loading them from disk into a high-performance `vk::raii::Buffer` with the `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT`. + +[source,cpp] +---- +// resource_manager.cpp +std::shared_ptr ResourceManager::loadMLWeights(const std::string& path) { + auto rawWeights = loadBinaryFile(path); + + // Create a staging buffer and then transfer to GPU-local memory + // for optimal cooperative matrix loading speed + return createGpuLocalBuffer( + rawWeights.size(), + vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + rawWeights.data() + ); +} +---- + +== Why These Changes? + +By placing the ML inference pass directly within our `Renderer` and using **Vulkan 1.4** synchronization (Synchronization 2), we ensure that our spatial refinement happens at the absolute last microsecond before the data is needed. This minimizes the **Motion-to-Photon** gap. Furthermore, by treating weights as standard engine resources, we can hot-reload different ML models (e.g., switching from a high-performance "Mobile" model to a high-fidelity "Desktop" model) without restarting the engine. + +This integration turns our engine from a simple rasterizer into a context-aware spatial platform capable of understanding and reacting to the user's physical environment. + +xref:03_refining_spatial_data.adoc[Previous] | xref:../18_Semantic_Occlusion/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc new file mode 100644 index 000000000..612e24992 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc @@ -0,0 +1,10 @@ +:pp: {plus}{plus} += Semantic Occlusion: Depth-Aware Real/Virtual Compositing + +In previous chapters, we learned how to ingest the physical world's geometry. But simply having that geometry isn't enough for a convincing experience. If you place a virtual robot on your floor, it should be hidden when your real-world coffee table is between you and the robot. This is the challenge of **Semantic Occlusion**. + +In this chapter, we will explore how to bridge the gap between real and virtual worlds through depth-aware occlusion and **Semantic Segmentation**. We'll learn how to use ML models to label reality at a pixel level—distinguishing "Human Hand" from "Table"—and how to use the Vulkan **Stencil Buffer** and **Depth-Aware Compositing** to ensure virtual objects "tuck behind" real-world ones with sub-pixel precision. + +By the end of this chapter, you'll be able to implement stable, flicker-free masking that makes virtual assets feel like they truly occupy the same physical space as the user's hands and furniture. + +xref:../17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Previous] | xref:02_ml_driven_segmentation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc new file mode 100644 index 000000000..f60b06913 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc @@ -0,0 +1,51 @@ +:pp: {plus}{plus} += ML-Driven Segmentation: Understanding Reality + +In standard Augmented Reality (AR), virtual objects are often drawn directly on top of the camera feed. This is known as **Naïve Compositing**, and it frequently leads to a jarring "Ghosting Effect"—where a virtual robot appears to float in front of a real table that should logically be occluding it. To fix this, we use **Semantic Segmentation**. + +== The Concept: Pixel-Wise Understanding + +Semantic segmentation is the process of labeling every pixel in the camera feed with a category. Unlike simple object detection, which just places a box around an item, segmentation provides a high-resolution, pixel-perfect understanding of the scene. + +1. **Input**: A raw RGB camera frame captured by the headset. +2. **ML Pass**: A neural network (often a **U-Net** or **Transformer** architecture) processes the frame. +3. **Output**: A **Segmentation Mask** where each pixel value corresponds to a category: 0 for Background, 1 for Table, 2 for Human Hand, etc. + +By understanding *what* occupies every pixel in the real world, our engine can make intelligent decisions about *how* to blend virtual content, ensuring that digital objects respect the physical boundaries of the user's room. + +== Implementing the Occlusion Mask + +In our Vulkan engine, we treat the output of this ML pass as a specialized texture: the **Occlusion Mask**. This texture is used in our final composite shader to gate the visibility of virtual pixels. + +Authoring this in **Slang**, we can efficiently sample the mask and perform conditional logic to handle complex real-world occlusion. + +[source,slang] +---- +// A Slang fragment shader utilizing a semantic mask for occlusion +[shader("pixel")] +float4 fragmentMain(V2P input) : SV_Target { + // 1. Sample the virtual scene color + float4 virtualColor = sceneTexture.Sample(input.uv); + + // 2. Sample the ML-generated semantic mask (using integer labels) + uint label = semanticMask.Sample(input.uv).r; + + // 3. Perform Semantic Occlusion + // If the pixel is labeled as "Human Hand", we hide the virtual object + // to allow the user's physical hand to appear "in front" of the digital content. + if (label == SEMANTIC_LABEL_HAND) { + // Transparent output allows the passthrough camera feed to show through + return float4(0, 0, 0, 0); + } + + return virtualColor; +} +---- + +== The Depth Buffer of Reality + +While semantic labels tell us *what* an object is, they don't always provide its exact distance. High-end spatial engines combine these labels with raw depth data from **LiDAR**. This enables **Depth-Aware Semantic Occlusion**, where a virtual robot can hide behind a real chair but still appear in front of a real-world wall further back in the room. + +By utilizing on-GPU inference, we generate these masks with minimal latency, ensuring that as the user moves their hand, the virtual world responds and "tucks behind" it instantly. + +xref:01_introduction.adoc[Previous] | xref:03_per_pixel_masking.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc new file mode 100644 index 000000000..72755c0d7 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc @@ -0,0 +1,48 @@ +:pp: {plus}{plus} += Per-Pixel Masking: Stable Compositing + +Even with a perfect semantic mask, compositing virtual objects with the real world is physically complex. The edges where a virtual asset meets a real-world object often appear "jittery" or suffer from "haloing"—a visual artifact where the background camera feed bleeds into the virtual foreground. To solve this, we use stable **Per-Pixel Masking** and high-fidelity compositing. + +== The Concept: The Compositing Equation + +In standard rendering, we use simple alpha blending to combine images. In Mixed Reality (**MR**), our "background" is the live camera feed (passthrough). To achieve realistic occlusion, we must apply our semantic mask as a **Gating Function**. + +1. **Hard Occlusion**: A binary decision (0 or 1). While simple, this leads to aliased, jagged edges that shimmer as the user's head moves. +2. **Soft Occlusion**: A smooth gradient at the boundaries. By "feathering" the mask, we can blend the virtual and real worlds naturally, hiding the slight imperfections of the ML segmentation pass. + +== Implementing Stencil-Based Masking + +In our Vulkan engine, we can use the **Stencil Buffer** to perform high-speed masking. This is more efficient than performing complex branching in a fragment shader, as the hardware can discard pixels before they ever reach the shading stage. + +[source,cpp] +---- +// Configuring stencil masking with vulkan-hpp +vk::StencilOpState stencilOp{ + .failOp = vk::StencilOp::eKeep, + .passOp = vk::StencilOp::eReplace, + .depthFailOp = vk::StencilOp::eKeep, + .compareOp = vk::CompareOp::eAlways, + .compareMask = 0xFF, + .writeMask = 0xFF, + .reference = 0x1 // Mark real-world occluders with 0x1 +}; + +// 1. Fill the stencil buffer using the ML segmentation mask +cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, 0x1); +cmd.draw(6, 1, 0, 0); // Full-screen quad or mesh-based occluder + +// 2. Render virtual scene where stencil is NOT 0x1 +cmd.setStencilCompareMask(vk::StencilFaceFlagBits::eFrontAndBack, 0xFF); +cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, 0x1); +// CompareOp::eNotEqual ensures we only draw on virtual pixels +---- + +== Temporal Stability: The Edge Jitter Problem + +Because ML masks are generated frame-by-frame, they can slightly flicker due to camera noise or changes in lighting. This creates **Temporal Instability** at the occlusion edges. + +To fix this, our engine implements **Temporal Reprojection**. We take the mask from the previous frame, re-project it using the current head pose (from OpenXR's predicted display time), and blend it with the new mask. This ensures that the boundary between reality and virtuality stays rock-steady, which is essential for maintaining user immersion and preventing motion sickness. + +By combining semantic understanding with stable per-pixel masking, we create an environment where virtual objects feel physically "locked" into the user's room. + +xref:02_ml_driven_segmentation.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..fbe0acd80 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc @@ -0,0 +1,60 @@ +:pp: {plus}{plus} += Incorporating Semantic Occlusion into the Engine + +Integrating **Semantic Occlusion** allows our `simple_game_engine` to realistically blend virtual assets with the physical room. We implement this by adding a masking pass to our `Renderer` and updating our final composition logic in `renderer_rendering.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Creating the Occlusion Mask Pass + +In `renderer_rendering.cpp`, we add a step that uses our ML segmentation results to fill the **Stencil Buffer**. This pass must happen after we've received the latest ML mask but before we draw our virtual scene geometry. + +[source,cpp] +---- +// renderer_rendering.cpp +void Renderer::executeOcclusionMaskPass(vk::raii::CommandBuffer& cmd, uint32_t frameIndex) { + if (!xrMode || !occlusionEnabled) return; + + // 1. Prepare to fill the stencil buffer + // We bind a simple pipeline that reads the ML mask texture + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *occlusionMaskPipeline); + + // 2. Set the stencil reference to 0x1 (meaning "Real World") + cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, 0x1); + + // 3. Draw a full-screen quad. The shader will discard fragments + // where the ML mask doesn't indicate an occluding object (like a hand). + cmd.draw(3, 1, 0, 0); // Using a single large triangle for the full screen +} +---- + +== Composition in Slang + +In our `composite.slang` shader, we use the stencil buffer's output to decide how to blend the virtual scene with the passthrough camera feed. Alternatively, we can use a dedicated texture if the hardware doesn't support stencil-based discard. + +[source,slang] +---- +// composite.slang +[shader("pixel")] +float4 fragmentMain(V2P input) : SV_Target { + // 1. Sample the virtual scene and the passthrough feed + float4 virtualColor = virtualScene.Sample(input.uv); + float4 passthroughColor = passthroughTexture.Sample(input.uv); + + // 2. Sample our refined occlusion mask + float mask = occlusionMask.Sample(input.uv).r; + + // 3. Perform the Final Blend + // If mask > 0.5, the real world is occluding. We use a smoothstep + // to provide a "soft" edge, reducing jitter. + float blendFactor = smoothstep(0.4, 0.6, mask); + + return lerp(virtualColor, passthroughColor, blendFactor); +} +---- + +== Why These Changes? + +By moving occlusion from a simple "on/off" switch to a dedicated pass in our `Renderer`, we allow our virtual objects to interact with the real world on a per-pixel basis. A virtual character can now reach "behind" a real-world object. Because our engine uses a modern **Synchronization 2** architecture, we can ensure the ML mask is ready before this pass begins without stalling the entire GPU. + +This pixel-wise awareness is what transforms a "Video Overlay" into a "Spatial Experience," where the boundary between virtual and physical becomes indistinguishable to the user's eye. + +xref:03_per_pixel_masking.adoc[Previous] | xref:../19_Platform_Divergence/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc new file mode 100644 index 000000000..92145e4bd --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc @@ -0,0 +1,10 @@ +:pp: {plus}{plus} += Platform Divergence: Desktop DirectMode vs. Mobile Tiled/ASW + +Spatial computing is unique because it spans two radically different hardware universes. On one end, we have the high-end **Desktop VR** PC, powered by a 400-watt GPU and a high-speed PCIe bus. On the other end, we have the **Mobile Standalone** headset, running on a mobile processor with a strict power budget of under 10 watts. + +As an engine developer, you cannot treat these platforms the same. A "standard" rendering loop that runs at 200 FPS on a desktop will likely drain a mobile battery in minutes or fail to maintain the stable 72Hz required for spatial comfort. + +In this chapter, we will explore the techniques required to master both platforms. We'll learn how to utilize **Direct Mode** and **HDR10** to saturate high-end GPUs, and how to use **Tile-Based Rendering (TBR)** and **Application SpaceWarp (ASW)** to extract every last drop of performance from mobile silicon. By the end of this chapter, you'll know how to architect your engine's backend to automatically scale its fidelity and efficiency based on the detected spatial hardware. + +xref:../18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Previous] | xref:02_desktop_high_end.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc new file mode 100644 index 000000000..57fcbe8d0 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc @@ -0,0 +1,40 @@ +:pp: {plus}{plus} += Desktop High-End: Unleashing the GPU + +On high-end desktop platforms, our spatial engine is largely unconstrained by battery life or strict thermal limits. This allows us to focus on absolute visual fidelity, utilizing the full power of modern GPUs to drive high-resolution headsets. The goal on desktop is **Saturation**—keeping the GPU's memory bus and compute units as full as possible to deliver the most immersive experience the hardware can achieve. + +== The Concept: Direct Mode + +In a standard application, your Vulkan windows are managed by the operating system's window manager (like **DWM** on Windows). This adds a layer of latency as the OS composites your window with other desktop elements. For spatial computing, this extra latency is unacceptable. + +We use **Direct Mode**. This allows the XR runtime to take exclusive control of the display output, bypassing the OS desktop compositor entirely. + +* **Latency Win**: By removing the OS compositor, we reduce frame delivery time by 1–2 frames, which is critical for **Motion-to-Photon** stability. +* **Strobe Synchronization**: Direct mode allows for perfect "display strobing." The headset's backlight flashes in precise sync with the GPU's swap signal, eliminating motion blur during rapid head movements. + +== HDR10 and 10-bit Color: Beyond the Screen + +Desktop GPUs have the bandwidth to support **High Dynamic Range (HDR)**. In XR, this is essential for physical realism. A virtual sun should be thousands of times brighter than a virtual candle. Without HDR, the engine must "tone-map" these values into the same narrow range, losing the feeling of scale. + +To implement this, our `VulkanContext` negotiates a 10-bit color format—such as `VK_FORMAT_A2B10G10R10_UNORM_PACK32`—with the OpenXR runtime. This provides over a billion colors, eliminating "banding" artifacts in dark spatial environments like night scenes or caves. + +== Leveraging High PCIe Bandwidth + +Because we have a high-speed **PCIe 4.0** or **5.0** link between the CPU and GPU, we can push massive amounts of data per frame without bottlenecking. + +* **Uncompressed LightFields**: We can upload gigabytes of plenoptic data directly to the GPU without stalling the render loop. +* **Ray Tracing**: We can afford the high overhead of building **Acceleration Structures** (AS) for complex, high-poly environments every frame, enabling real-time spatial reflections and global illumination. + +[source,cpp] +---- +// Configuring HDR10 for high-end desktop XR with vulkan-hpp +vk::SwapchainCreateInfoKHR createInfo{ + .imageFormat = vk::Format::eA2b10g10r10UnormPack32, + .imageColorSpace = vk::ColorSpaceKHR::eHdr10St2084EXT, + // ... other parameters ... +}; +---- + +On the desktop, the spatial mindset is about utilizing every watt and every gigabyte of bandwidth to dissolve the boundary between the user and the virtual world. + +xref:01_introduction.adoc[Previous] | xref:03_mobile_mastery.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc new file mode 100644 index 000000000..747db4298 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc @@ -0,0 +1,32 @@ +:pp: {plus}{plus} += Mobile Mastery: Conquering the Thermal Wall + +Mobile spatial computing, on standalone headsets like the Meta Quest or Vive Focus, is an exercise in extreme efficiency. Unlike a desktop PC where we have hundreds of watts of power, a mobile headset must run its entire engine, tracking system, and display on a power budget of roughly 5–10 watts. + +On mobile, the spatial mindset is about **Efficiency**—minimizing every memory access and every pixel calculation to extract maximum performance from the silicon. + +== The Concept: Tile-Based Rendering (TBR) + +Most mobile GPUs (like those from Qualcomm, Arm, or Imagination) use a **Tile-Based Rendering** architecture. Instead of rendering the whole screen at once, the GPU splits the screen into tiny tiles (e.g., 16x16 or 32x32 pixels). It then processes each tile entirely within high-speed, low-power on-chip memory (**LDS** or **SRAM**) before writing the final result back to main memory. + +In mobile spatial computing, **Discarding is Winning**. + +1. **Load Op**: Does the tile need to be cleared (`eClear`) or loaded from VRAM (`eLoad`)? +2. **Store Op**: Does the final color need to be written back to VRAM (`eStore`) or discarded (`eDontCare`)? + +If you don't need the depth buffer for the next frame, you MUST set its store op to `eDontCare`. This prevents the GPU from wasting battery power moving megabytes of depth data across the bus, which is the most power-hungry operation on a mobile chip. + +== Application SpaceWarp (ASW): Synthetic Frames + +To maintain a smooth 72Hz or 90Hz experience on a mobile chip, we often use **Application SpaceWarp**. This is a hardware-accelerated technique that allows the engine to render at half-framerate (e.g., 36 FPS) while the XR runtime generates every second frame synthetically. + +* **How it works**: The engine provides **Motion Vectors** and a depth map to the runtime. These tell the compositor how each pixel moved since the last frame. +* **The Result**: The GPU only performs heavy shading work for half the frames, allowing for significantly more complex scenes and higher-fidelity spatial logic. + +== Efficient Spatial Sync + +Mobile headsets use **Unified Memory**, where the CPU and GPU share the same physical RAM. While this simplifies zero-copy hand-offs, it also means they are competing for the same **Memory Bandwidth**. + +To master mobile spatial computing, our engine must be highly surgical with its memory access. Every texture sample and every buffer write must be justified. By focusing on tile-local memory and synthetic frame generation, we can deliver high-quality spatial experiences that run for hours on a single battery charge. + +xref:02_desktop_high_end.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..844ac11e8 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc @@ -0,0 +1,64 @@ +:pp: {plus}{plus} += Incorporating Platform Divergence into the Engine + +Integrating **Platform Divergence** allows our `simple_game_engine` to automatically optimize its spatial pipeline for both high-end desktop and low-power mobile hardware. We achieve this by adding a configuration layer to our `Renderer` in `renderer_core.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Defining Hardware Profiles + +In `renderer_core.cpp`, we introduce a method to apply rendering presets based on the detected hardware. This logic ensures that our engine scales its fidelity to match the platform's constraints. + +[source,cpp] +---- +// renderer_core.cpp +void Renderer::applySpatialPlatformPresets() { + // Note: platform is our Engine's platform abstraction + if (platform->isMobile()) { + // 1. Mobile Mastery: Optimize for Tile-Based GPUs + renderScale = 0.75f; // Lower initial resolution for stability + + // 2. Explicitly set Load/Store ops for tile memory efficiency + // Discarding depth/multisample data saves massive bandwidth + mainColorAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + mainDepthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + + // 3. Enable Application SpaceWarp for synthetic frame generation + xrConfig.enableASW = true; + } else { + // 4. Desktop High-End: Maximum Fidelity + renderScale = 1.0f; + useHDR10 = true; // Use 10-bit color formats + + // Enable high-bandwidth features like Ray Query + vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryFeatures{}; + // ... request features from the LUID-matched physical device ... + } +} +---- + +== Handling Direct Mode on Desktop + +For desktop headsets, we must ensure our engine's windowing system (GLFW) doesn't interfere with the headset's **Direct Mode** display. In `main.cpp`, we configure our window creation logic. + +[source,cpp] +---- +// main.cpp +void setupEngineWindow() { + if (engine.isXRMode()) { + // On Desktop, the headset is a "hidden" display. + // We create a small "Mirror Window" for the monitor + // while the main spatial rendering bypasses the OS compositor. + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + engine.createMirrorWindow("XR Mirror", 1280, 720); + } else { + engine.createStandardWindow("Simple Engine", 1920, 1080); + } +} +---- + +== Why These Changes? + +By implementing platform-specific presets and Direct Mode handling, we ensure that our engine remains performant regardless of whether it's running on a high-end desktop GPU or a mobile SOC. The `simple_game_engine` already utilizes a modular `Platform` abstraction, which we now leverage to swap between desktop-specific logic (like 10-bit HDR) and mobile-specific optimizations (like **Tile-Based** discarding). + +This architectural flexibility is what allows our spatial applications to scale from the highest possible fidelity to the most efficient mobile standalone performance without rewriting our core rendering logic. + +xref:03_mobile_mastery.adoc[Previous] | xref:../20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc new file mode 100644 index 000000000..93665359d --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc @@ -0,0 +1,12 @@ +:pp: {plus}{plus} += Advanced Spatial Diagnostics & CI/CD + +In this final technical chapter, we've reached the point where our engine is feature-complete. But in the world of spatial computing, a "feature-complete" engine is useless if it doesn't maintain absolute stability and 90+ FPS across hundreds of varied headset and GPU combinations. + +Debugging a spatial engine is notoriously difficult. Because the XR runtime and its internal compositor are often "black boxes," it can be challenging to determine if a visual artifact is caused by your engine's logic, a synchronization race condition, or the runtime's own reprojection algorithm. + +In this chapter, we will explore the tools and techniques required to peel back these layers. We'll learn how to use **Spatial Debugging** tools like RenderDoc to inspect multiview buffers, how to implement **Headless CI/CD** (Continuous Integration/Continuous Deployment) using the OpenXR Simulator, and how to utilize **AI Vision Analysis** to automatically detect rendering regressions before they ever reach a physical headset. + +By the end of this chapter, you'll have the diagnostic toolkit required to ship professional-grade spatial applications that are both visually stunning and rock-steady. + +xref:../19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Previous] | xref:02_spatial_debugging.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc new file mode 100644 index 000000000..9a0972115 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc @@ -0,0 +1,39 @@ +:pp: {plus}{plus} += Spatial Debugging: Peering into the Black Box + +Debugging a spatial engine is fundamentally different from a standard 2D application. Because the XR runtime and its internal compositor are often black boxes, it can be hard to determine if a rendering error is in your engine, the OpenXR handshake, or the compositor's own reprojection logic. To solve this, we use **Spatial Debugging**. + +== The Concept: Object Tagging + +The **XR_EXT_debug_utils** extension allows you to "label" your OpenXR resources. This is functionally identical to Vulkan's `VK_EXT_debug_utils`. By assigning names to your **Action Sets**, **Spaces**, and **Swapchains**, you can see these names in external tools rather than just raw memory addresses. + +[source,cpp] +---- +// Tagging an OpenXR swapchain for easier debugging +XrDebugUtilsObjectNameInfoEXT nameInfo{XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT}; +nameInfo.objectType = XR_OBJECT_TYPE_SWAPCHAIN; +nameInfo.objectHandle = reinterpret_cast(xrSwapchain); +nameInfo.objectName = "Main World Spatial Swapchain"; + +// Now, tools like RenderDoc will display this name for the resource +xrSetDebugUtilsObjectNameEXT(instance, &nameInfo); +---- + +== RenderDoc for Spatial Engines + +**RenderDoc** is the gold standard for Vulkan debugging, and it provides excellent support for modern spatial pipelines. + +1. **Multi-View Inspection**: You can inspect each layer of your multiview swapchain individually. If one eye is black but the other is rendering, RenderDoc can show you the state of each `SV_ViewID` invocation. +2. **Resource State Inspection**: You can verify that your `vk::RenderingInfo` has the correct `viewMask` and that your **Synchronization 2** barriers are actually waiting for the correct stages. +3. **Compositor Insights**: Some advanced headsets allow RenderDoc to capture the compositor's final warped and blended frame, letting you see exactly how your layers are being manipulated. + +== Frame Analysis: Finding the Stutter + +In spatial computing, a "Bug" is often not a crash, but a **Dropped Frame**. A single dropped frame can cause a perceptible "stutter" that breaks immersion. + +* **GPU Profiling**: Use tools like **NVIDIA Nsight Graphics** or **Radeon GPU Profiler** to see if your **Late Latching** wait is taking too long. +* **OpenXR Trace**: Most runtimes provide a tracing tool (like the Meta Quest Metrics Tool or SteamVR System Report) that shows **Compositor Pacing**. If your "App GPU Time" is low but the "Compositor Frame Missed" count is high, you likely have a synchronization race condition in your **Wait-Acquire-Release** loop. + +By instrumenting your code with debug labels and utilizing frame analysis tools, you can ensure your spatial experience remains rock-steady and comfortable for the user. + +xref:01_introduction.adoc[Previous] | xref:03_automated_qa.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc new file mode 100644 index 000000000..d6d305aba --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc @@ -0,0 +1,36 @@ +:pp: {plus}{plus} += Automated QA: Headless CI/CD for Spatial Engines + +Testing a spatial engine is traditionally a manual, time-consuming process. Someone must put on a headset, walk around the physical room, and manually check for rendering artifacts. For professional engine development, this is a major bottleneck. We need **Automated QA** that can run in a **Headless** CI/CD (Continuous Integration / Continuous Deployment) environment. + +== The Concept: The Headless Runtime + +A **Headless Runtime** (such as the **OpenXR Simulator** or **Null Runtime**) allows our engine to run without a physical headset or even a physical GPU (using software Vulkan implementations like **SwiftShader**). + +1. **Mock Tracking Data**: The simulator provides pre-recorded or scripted tracking data (poses and controller inputs). +2. **Swapchain Capture**: The engine renders frames into the OpenXR swapchain as usual, but the simulator captures these frames directly to disk rather than displaying them. +3. **Visual Validation**: We compare these captured frames against "Gold Standard" baseline images to ensure no rendering regressions have been introduced by a code change. + +== AI Vision Analysis for Regressions + +Traditional "Pixel-Perfect" image comparison often fails in spatial computing due to subtle differences in reprojection, slight floating-point variations in view matrices, or hardware-specific MSAA patterns. + +Instead, we use **AI Vision Models**. We feed the rendered frames from our CI/CD pipeline into a neural network trained to detect specific **Spatial Artifacts**: + +* **Asymmetric Frustum Errors**: Detecting if the projection matrices are causing "stretching" or "pinching" at the edges of the view. +* **Multiview Inconsistencies**: Checking if an object is missing from one eye but present in the other. +* **Temporal Mask Flicker**: Analyzing a sequence of frames to identify instability in **Semantic Occlusion** masks. + +By using AI, we can automate the "someone put on the headset" step, catching bugs within minutes of a pull request being submitted. + +== Performance Monitoring in CI + +Finally, we track **Spatial Performance Metrics** in every build to prevent performance "creep." + +* **Submission Latency**: The total time between `xrWaitFrame` and `xrEndFrame`. +* **Wait-to-Begin Gap**: The time the CPU spends waiting for the late-latching sync point. +* **Memory Footprint**: Tracking the VRAM usage of our 4D **LightField** and **Semantic Occlusion** buffers. + +By combining headless simulation, AI-driven visual analysis, and automated performance tracking, we ensure that our spatial engine remains visually stunning, stable, and efficient across every commit and every hardware target. + +xref:02_spatial_debugging.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc new file mode 100644 index 000000000..024484541 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc @@ -0,0 +1,63 @@ +:pp: {plus}{plus} += Incorporating Spatial Diagnostics into the Engine + +Integrating **Spatial Diagnostics & CI/CD** allows our `simple_game_engine` to maintain absolute stability through automated testing and deep-dive resource inspection. We achieve this by adding telemetry hooks to our `Renderer` and configuring a "Headless" mode in `engine.cpp`. *This is currently left as an exercise for the reader to implement.* + +== Adding Diagnostic Labels + +In `renderer_core.cpp`, we extend our resource creation logic to include OpenXR and Vulkan debug labels. This ensures that when we capture a frame in RenderDoc, we can see exactly what each resource represents. + +[source,cpp] +---- +// renderer_core.cpp +void Renderer::setupSpatialResourceLabels(const XrInstance& xrInstance) { + if (!xrMode || !hasDebugUtilsSupport) return; + + // 1. Tag the spatial swapchain for RenderDoc identification + XrDebugUtilsObjectNameInfoEXT nameInfo{ + .type = XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, + .objectType = XR_OBJECT_TYPE_SWAPCHAIN, + .objectHandle = reinterpret_cast(spatialSwapchain), + .objectName = "Spatial Swapchain" + }; + + xrSetDebugUtilsObjectNameEXT(xrInstance, &nameInfo); + + // 2. Add a diagnostic hook to our main command buffer + // This allows us to track the frame's progress in a GPU capture + vk::DebugUtilsLabelEXT frameLabel{ .pLabelName = "Spatial Frame Rendering" }; + commandBuffer.beginDebugUtilsLabelEXT(frameLabel); +} +---- + +== Enabling Automated CI/CD Headless Mode + +For automated testing, we configure our engine to run in a "Headless" mode within `engine.cpp`. In this state, we swap the real OpenXR runtime for a simulator and save screenshots for AI vision analysis. + +[source,cpp] +---- +// engine.cpp +void Engine::runSpatialCITest(uint32_t totalFrames) { + // 1. Configure the XR Context for the Headless Simulator + xrContext.enableSimulatorMode("Baseline_Tests.json"); + + for (uint32_t i = 0; i < totalFrames; ++i) { + // 2. Perform the standard update and render + update(fixedTimeStep); + render(); + + // 3. Capture every 100th frame for AI-driven visual validation + if (i % 100 == 0) { + renderer->captureFrameToDisk("CI_Spatial_Capture_" + std::to_string(i) + ".png"); + } + } +} +---- + +== Why These Changes? + +By building diagnostic hooks and automated testing directly into the engine, we ensure that new spatial features (like **LightField** synthesis or **Late Latching**) don't introduce visual regressions or performance regressions. The `simple_game_engine` utilizes these telemetry points in its internal **Watchdog** system, alerting developers if the **Motion-to-Photon** gap exceeds the target threshold. + +This "Spatial-First" CI/CD approach is what allows professional teams to maintain high-fidelity experiences across varied hardware targets while keeping the codebase stable and reliable. + +xref:03_automated_qa.adoc[Previous] | xref:../conclusion.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc new file mode 100644 index 000000000..2b89368a3 --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc @@ -0,0 +1,10 @@ +:pp: {plus}{plus} += Conclusion + +Congratulations on completing the **OpenXR and Vulkan 1.4 Spatial Computing** tutorial series! We've covered a vast amount of ground, from the initial handshake between the two APIs to advanced topics like LightField Theory and Plenoptic Synthesis. + +Through the "Incorporating into the Engine" sections at the end of each chapter, you've seen how to take these theoretical concepts and apply them to a real-world renderer like our `simple_game_engine`. + +Spatial computing is an ever-evolving field, and we encourage you to continue exploring, experimenting, and pushing the boundaries of what's possible with Vulkan 1.4 and OpenXR. + +xref:20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Previous] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc new file mode 100644 index 000000000..1c26e70cb --- /dev/null +++ b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc @@ -0,0 +1,57 @@ +:pp: {plus}{plus} += OpenXR and Vulkan 1.4 Spatial Computing: Introduction + +Welcome to the frontiers of spatial computing. In this tutorial series, we are going to bridge the gap between traditional 2D engine architecture and the immersive, low-latency requirements of Virtual Reality (VR), Augmented Reality (AR), and Mixed Reality (MR)—collectively known as XR. + +== Prerequisites + +Before beginning this tutorial, you should have a solid foundation in both the Vulkan API and engine architecture. Specifically, we assume you have completed: + +1. **xref:../00_Introduction.adoc[The Core Vulkan Tutorial]**: You should be comfortable with the core concepts of Vulkan, including instance creation, physical devices, logical devices, and the basic graphics pipeline. +2. **xref:../Building_a_Simple_Engine/introduction.adoc[Building a Simple Engine]**: This series builds directly upon the code and architectural patterns established in the engine tutorial. We will be using the `Simple Engine` codebase as our starting point. +3. **Modern C{pp} and RAII**: We utilize `vulkan-hpp` and RAII (Resource Acquisition Is Initialization) throughout the series. You should be familiar with these patterns to follow the C{pp} integration sections. + +== The OpenXR Ecosystem: Runtimes and Loaders + +Before we dive into the code, it is important to understand the two core components that make OpenXR work: the **Loader** and the **Runtime**. + +1. **The OpenXR Loader**: This is a small, vendor-neutral library that your application links against. Its primary responsibility is to act as a traffic cop. When your engine starts, the loader looks at the system's registry or environment variables to find the currently active XR runtime and redirects all your API calls to it. +2. **The OpenXR Runtime**: This is the heavy lifter provided by the hardware manufacturer (such as Meta, Valve, Microsoft, or the open-source Monado project). The runtime contains the actual implementation of the OpenXR spec, including the drivers for the cameras, displays, and tracking sensors. + +To follow this tutorial, you will need to have an OpenXR-compatible environment set up on your development machine. This typically involves installing the SDK and ensuring a runtime is active. + +[TIP] +==== +For the most up-to-date installation instructions and to download the necessary headers and loader libraries, we highly recommend visiting the official **link:https://github.com/KhronosGroup/OpenXR-SDK-Source[Khronos OpenXR-SDK-Source]** repository and following the **link:https://www.khronos.org/registry/OpenXR/specs/1.1/html/xrspec.html[OpenXR Specification]** or the excellent **link:https://www.openxr-tutorial.com/[OpenXR Tutorial]** for platform-specific setup guides. +==== + +== Why OpenXR? + +In the early days of XR, developers had to write custom backends for every headset on the market. OpenXR changed all that by providing a cross-platform, high-performance API that allows a single application to run on devices ranging from the Meta Quest to the Valve Index and the Microsoft HoloLens. + +But OpenXR isn't just a wrapper. It is a sophisticated state machine that manages hardware poses, predictive frame timing, and specialized swapchains that are owned by the XR runtime itself. To use it effectively, we have to rethink how our engine handles its main loop, its memory, and its synchronization. + +== The Vulkan 1.4 Advantage + +Vulkan 1.4 brings several critical features to the table that are particularly powerful for spatial computing: + +* **Timeline Semaphores**: Essential for the complex, cross-process synchronization required between your engine and the XR compositor. +* **Dynamic Rendering**: Provides a flexible, lightweight rendering path for stereo views, avoiding the rigid state of legacy Render Passes. +* **Synchronization 2**: Simplifies the ownership transfers and barriers needed for low-latency late latching. +* **Maintenance Extensions**: Providing better control over memory and resource visibility across hardware boundaries. + +== What We Will Build + +Throughout this series, we will cover the entire lifecycle of an XR frame: + +1. **The Handshake**: Connecting OpenXR to our Vulkan context using LUID matching and mandatory extensions. +2. **Resource Management**: Wrapping runtime-owned images into our RAII-based engine abstractions. +3. **The Predictive Loop**: Mastering frame timing to ensure that what the user sees matches exactly where their head is located at the moment of display. +4. **Spatial Shaders**: Using Slang to author efficient multiview and foveated rendering shaders. +5. **Advanced Sensing**: Ingesting environmental meshes and using ML inference to refine spatial data. + +By the end of this tutorial, you won't just have a working XR application; you will have a deep understanding of the architectural patterns required for high-performance spatial computing in the modern Vulkan ecosystem. + +Let's get started with the first step: the OpenXR-Vulkan 1.4 Handshake. + +xref:02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Next] From 704e33fc82738daeae828b57100c42622bca01d4 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 16 Mar 2026 22:41:51 -0700 Subject: [PATCH 02/23] Add comprehensive OpenXR and Vulkan 1.4 Spatial Computing chapter navigation Add navigation structure for new OpenXR-Vulkan spatial computing guide covering 20 chapters: OpenXR-Vulkan handshake, runtime-owned swapchains, dynamic rendering, predictive frame loop, late latching, action spaces, Slang shaders, quad-views with foveated rendering, variable rate shading, canted displays, CAVE architecture, warp and blend, lightfield theory, plenoptic synthesis, scene understanding, ML inference, semantic occlusion, platform divergence, and spatial diagnostics. Each chapter includes introduction, technical deep-dives, and engine integration sections. --- antora/modules/ROOT/nav.adoc | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 89c2dc6fd..19625144b 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -149,3 +149,104 @@ *** xref:Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc[Robustness2] ** Appendix *** xref:Building_a_Simple_Engine/Appendix/appendix.adoc[Appendix] + +* OpenXR and Vulkan 1.4 Spatial Computing +** xref:OpenXR_Vulkan_Spatial_Computing/introduction.adoc[Introduction] +** The OpenXR-Vulkan 1.4 Handshake +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[System Integration] +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Hardware Alignment (LUID)] +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Vulkan 1.4 Feature Requirements] +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Runtime-Owned Swapchains +*** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc[External Image Negotiation] +*** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc[RAII Resource Integration] +*** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc[Memory Ownership Lifecycle] +*** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Dynamic Rendering for Spatial Views +*** xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc[Rendering into Spatial Swapchains] +*** xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc[Stereo Viewport & Scissor Management] +*** xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** The Predictive Frame Loop +*** xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc[The XR Lifecycle] +*** xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc[Display Time Prediction] +*** xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Late Latching +*** xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc[The Last-Second Update] +*** xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc[Implementation] +*** xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Action Spaces and Input +*** xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc[The OpenXR Action System] +*** xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc[Space Manifolds] +*** xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Slang for Spatial Shaders +*** xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc[Native Multiview (N=2)] +*** xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc[Slang Architecture] +*** xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Quad-Views and Foveated Rendering +*** xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc[Primary Stereo with Insets] +*** xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Multi-Layer Composition] +*** xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Variable Rate Shading +*** xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc[Fragment Density Control] +*** xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc[Gaze-Driven Logic] +*** xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Canted Displays +*** xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc[Non-Parallel Projections] +*** xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc[Viewport Swizzling] +*** xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** CAVE Architecture +*** xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc[Projector-Based Spatial Tech] +*** xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc[Hardware Sync] +*** xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Warp and Blend +*** xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc[Geometric Correction] +*** xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc[Post-Process Warping] +*** xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** LightField Theory +*** xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc[4D LightField Representation] +*** xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc[High-Density View Arrays] +*** xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Plenoptic Synthesis +*** xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc[Synthesis Shaders] +*** xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc[Ray Traced Synthesis] +*** xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Scene Understanding +*** xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc[Environmental Ingestion] +*** xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc[Zero-Copy Hand-off] +*** xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** ML Inference for Spatial Data +*** xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc[On-GPU Inference] +*** xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc[Refining Spatial Data] +*** xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Semantic Occlusion +*** xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc[ML-Driven Segmentation] +*** xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc[Per-Pixel Masking] +*** xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Platform Divergence +*** xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc[Desktop High-End] +*** xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc[Mobile Mastery] +*** xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** Spatial Diagnostics +*** xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Introduction] +*** xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc[Spatial Debugging] +*** xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc[Automated QA] +*** xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Incorporating into the Engine] +** xref:OpenXR_Vulkan_Spatial_Computing/conclusion.adoc[Conclusion] From 90c7206425aa2acda81e0f7eea9b13c9ea1f6542 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 16 Mar 2026 22:53:36 -0700 Subject: [PATCH 03/23] Remove imgui_tables.cpp and imgui_widgets.cpp from CMakeLists.txt build sources --- attachments/openxr_engine/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index 13a3b4c66..9cc226607 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -148,8 +148,6 @@ set(SOURCES_COMMON ${SIMPLE_ENGINE_DIR}/imgui_system.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui_draw.cpp - ${SIMPLE_ENGINE_DIR}/imgui/imgui_tables.cpp - ${SIMPLE_ENGINE_DIR}/imgui/imgui_widgets.cpp ${SIMPLE_ENGINE_DIR}/vulkan_device.cpp ${SIMPLE_ENGINE_DIR}/pipeline.cpp ${SIMPLE_ENGINE_DIR}/descriptor_manager.cpp From 0291b93c4981650ca015fa0fe0e305c570cc17b5 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 17 Mar 2026 00:47:37 -0700 Subject: [PATCH 04/23] Migrate from OpenXR C++ bindings to C API and fix CMake build configuration Replace openxr.hpp C++ wrapper with native openxr.h C API throughout xr_context. Add OpenXR::OpenXR alias for openxr_loader target compatibility. Fix memory_pool.cpp source path and Assets copy destination. Load Vulkan extension function pointers explicitly via xrGetInstanceProcAddr. Implement proper LUID extraction from OpenXR-selected physical device using VkPhysicalDeviceIDProperties. Replace XrGuidMSFT with XrUuidMSFT for spatial mesh structure. Update all OpenXR handle types from C++ wrappers to native C types (XrInstance, XrSession, XrSpace, XrAction, XrSwapchain). Convert all OpenXR API calls from method-style to function-style. Initialize views vector with proper XR_TYPE_VIEW structure type. --- attachments/openxr_engine/CMakeLists.txt | 7 +- attachments/openxr_engine/memory_pool.cpp | 589 +++++++++++++ attachments/openxr_engine/memory_pool.h | 218 +++++ attachments/openxr_engine/renderer.h | 40 +- attachments/openxr_engine/renderer_core.cpp | 5 +- .../openxr_engine/renderer_rendering.cpp | 30 +- .../openxr_engine/renderer_resources.cpp | 162 ++-- attachments/openxr_engine/xr_context.cpp | 831 ++++++++---------- attachments/openxr_engine/xr_context.h | 40 +- 9 files changed, 1341 insertions(+), 581 deletions(-) create mode 100644 attachments/openxr_engine/memory_pool.cpp create mode 100644 attachments/openxr_engine/memory_pool.h diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index 9cc226607..f8f2388de 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -27,6 +27,9 @@ find_package(VulkanHpp REQUIRED) # OpenXR is REQUIRED for this engine variant find_package(OpenXR REQUIRED) +if (NOT TARGET OpenXR::OpenXR AND TARGET OpenXR::openxr_loader) + add_library(OpenXR::OpenXR ALIAS OpenXR::openxr_loader) +endif() if(ENABLE_CPP20_MODULE) # Set up Vulkan C++ module @@ -135,7 +138,7 @@ set(SOURCES_COMMON ${SIMPLE_ENGINE_DIR}/renderer_compute.cpp ${SIMPLE_ENGINE_DIR}/renderer_utils.cpp ${SIMPLE_ENGINE_DIR}/renderer_ray_query.cpp - ${SIMPLE_ENGINE_DIR}/memory_pool.cpp + memory_pool.cpp ${SIMPLE_ENGINE_DIR}/resource_manager.cpp ${SIMPLE_ENGINE_DIR}/entity.cpp ${SIMPLE_ENGINE_DIR}/component.cpp @@ -227,7 +230,7 @@ endif() if(EXISTS ${SIMPLE_ENGINE_DIR}/Assets) if (NOT ANDROID) add_custom_command(TARGET OpenXREngine POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${SIMPLE_ENGINE_DIR}/Assets ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Assets + COMMAND ${CMAKE_COMMAND} -E copy_directory ${SIMPLE_ENGINE_DIR}/Assets ${CMAKE_CURRENT_BINARY_DIR}/Assets COMMENT "Copying Assets from Simple Engine to output directory" ) endif() diff --git a/attachments/openxr_engine/memory_pool.cpp b/attachments/openxr_engine/memory_pool.cpp new file mode 100644 index 000000000..32c603332 --- /dev/null +++ b/attachments/openxr_engine/memory_pool.cpp @@ -0,0 +1,589 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "memory_pool.h" +#include +#include +#include +#include + +MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice) : device(device), physicalDevice(physicalDevice) { +} + +MemoryPool::~MemoryPool() { + // RAII will handle cleanup automatically + std::lock_guard lock(poolMutex); + pools.clear(); +} + +bool MemoryPool::initialize() { + std::lock_guard lock(poolMutex); + + try { + // Configure default pool settings based on typical usage patterns + + // Vertex buffer pool: Large allocations, device-local (increased for large models like bistro) + configurePool( + PoolType::VERTEX_BUFFER, + 128 * 1024 * 1024, + // 128MB blocks (doubled) + 4096, + // 4KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Index buffer pool: Medium allocations, device-local (increased for large models like bistro) + configurePool( + PoolType::INDEX_BUFFER, + 64 * 1024 * 1024, + // 64MB blocks (doubled) + 2048, + // 2KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Uniform buffer pool: Small allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors + configurePool( + PoolType::UNIFORM_BUFFER, + 4 * 1024 * 1024, + // 4MB blocks + 64, + // 64B allocation units (aligned to nonCoherentAtomSize) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Staging buffer pool: Variable allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors + configurePool( + PoolType::STAGING_BUFFER, + 16 * 1024 * 1024, + // 16MB blocks + 64, + // 64B allocation units (aligned to nonCoherentAtomSize) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Texture image pool: Use moderate block sizes to reduce allocation failures on mid-range GPUs + configurePool( + PoolType::TEXTURE_IMAGE, + 64 * 1024 * 1024, + // 64MB blocks (smaller blocks reduce contiguous allocation pressure) + 4096, + // 4KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to initialize memory pool: " << e.what() << std::endl; + return false; + } +} + +void MemoryPool::configurePool( + const PoolType poolType, + const vk::DeviceSize blockSize, + const vk::DeviceSize allocationUnit, + const vk::MemoryPropertyFlags properties) { + PoolConfig config; + config.blockSize = blockSize; + config.allocationUnit = allocationUnit; + config.properties = properties; + + poolConfigs[poolType] = config; +} + +uint32_t MemoryPool::findMemoryType(const uint32_t typeFilter, const vk::MemoryPropertyFlags properties) const { + const vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && + (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); +} + +std::unique_ptr MemoryPool::createMemoryBlock(PoolType poolType, vk::DeviceSize size, vk::MemoryAllocateFlags allocFlags) { + auto configIt = poolConfigs.find(poolType); + if (configIt == poolConfigs.end()) { + throw std::runtime_error("Pool type not configured"); + } + + const PoolConfig& config = configIt->second; + + // Use the larger of the requested size or configured block size + const vk::DeviceSize blockSize = std::max(size, config.blockSize); + + // Create a dummy buffer to get memory requirements for the memory type + vk::BufferCreateInfo bufferInfo{ + .size = blockSize, + .usage = vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer | + vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eTransferSrc | + vk::BufferUsageFlagBits::eTransferDst, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer dummyBuffer(device, bufferInfo); + vk::MemoryRequirements memRequirements = dummyBuffer.getMemoryRequirements(); + + uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, config.properties); + + // Allocate the memory block using the device-required size + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = memoryTypeIndex + }; + + // Add allocation flags (e.g., VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT) if needed + vk::MemoryAllocateFlagsInfo flagsInfo{}; + if (allocFlags != vk::MemoryAllocateFlags{}) { + flagsInfo.flags = allocFlags; + allocInfo.pNext = &flagsInfo; + } + + // Create MemoryBlock with proper initialization to avoid default constructor issues + auto block = std::unique_ptr(new MemoryBlock{ + .memory = vk::raii::DeviceMemory(device, allocInfo), + .size = memRequirements.size, + .used = 0, + .memoryTypeIndex = memoryTypeIndex, + .isMapped = false, + .mappedPtr = nullptr, + .freeList = {}, + .allocationUnit = config.allocationUnit + }); + + // Map memory if it's host-visible + block->isMapped = (config.properties & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; + if (block->isMapped) { + block->mappedPtr = block->memory.mapMemory(0, memRequirements.size); + } else { + block->mappedPtr = nullptr; + } + + // Initialize a free list based on the actual allocated size + const size_t numUnits = static_cast(block->size / config.allocationUnit); + block->freeList.resize(numUnits, true); // All units initially free + + return block; +} + +std::unique_ptr MemoryPool::createMemoryBlockWithType(PoolType poolType, vk::DeviceSize size, uint32_t memoryTypeIndex, vk::MemoryAllocateFlags allocFlags) { + auto configIt = poolConfigs.find(poolType); + if (configIt == poolConfigs.end()) { + throw std::runtime_error("Pool type not configured"); + } + const PoolConfig& config = configIt->second; + + // Allocate the memory block with the exact requested size + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = size, + .memoryTypeIndex = memoryTypeIndex + }; + + // Add allocation flags (e.g., VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT) if needed + vk::MemoryAllocateFlagsInfo flagsInfo{}; + if (allocFlags != vk::MemoryAllocateFlags{}) { + flagsInfo.flags = allocFlags; + allocInfo.pNext = &flagsInfo; + } + + // Determine properties from the chosen memory type + const auto memProps = physicalDevice.getMemoryProperties(); + if (memoryTypeIndex >= memProps.memoryTypeCount) { + throw std::runtime_error("Invalid memoryTypeIndex for createMemoryBlockWithType"); + } + const vk::MemoryPropertyFlags typeProps = memProps.memoryTypes[memoryTypeIndex].propertyFlags; + + auto block = std::unique_ptr(new MemoryBlock{ + .memory = vk::raii::DeviceMemory(device, allocInfo), + .size = size, + .used = 0, + .memoryTypeIndex = memoryTypeIndex, + .isMapped = false, + .mappedPtr = nullptr, + .freeList = {}, + .allocationUnit = config.allocationUnit + }); + + block->isMapped = (typeProps & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; + if (block->isMapped) { + block->mappedPtr = block->memory.mapMemory(0, size); + } + + const size_t numUnits = static_cast(block->size / config.allocationUnit); + block->freeList.resize(numUnits, true); + + return block; +} + +std::pair MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace(poolType).first; + } + + auto& poolBlocks = poolIt->second; + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for size alignment) + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + + // Search existing blocks for sufficient free space with proper offset alignment + for (const auto& block : poolBlocks) { + const vk::DeviceSize unit = config.allocationUnit; + const size_t totalUnits = block->freeList.size(); + + size_t i = 0; + while (i < totalUnits) { + // Ensure starting unit produces an offset aligned to 'alignment' + vk::DeviceSize startOffset = static_cast(i) * unit; + if ((alignment > 0) && (startOffset % alignment != 0)) { + // Advance i to the next unit that aligns with 'alignment' + const vk::DeviceSize remainder = startOffset % alignment; + const vk::DeviceSize advanceBytes = alignment - remainder; + const size_t advanceUnits = static_cast((advanceBytes + unit - 1) / unit); + i += std::max(advanceUnits, 1); + continue; + } + + // From aligned i, check for consecutive free units + size_t consecutiveFree = 0; + size_t j = i; + while (j < totalUnits && block->freeList[j] && consecutiveFree < requiredUnits) { + ++consecutiveFree; + ++j; + } + + if (consecutiveFree >= requiredUnits) { + return {block.get(), i}; + } + + // Move past the checked range + i = (j > i) ? j : (i + 1); + } + } + + // No suitable block found; create a new one on demand (no hard limits, allowed during rendering) + try { + auto newBlock = createMemoryBlock(poolType, alignedSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << "Created new memory block (pool type: " + << static_cast(poolType) << ")" << std::endl; + return {poolBlocks.back().get(), 0}; + } catch (const std::exception& e) { + std::cerr << "Failed to create new memory block: " << e.what() << std::endl; + return {nullptr, 0}; + } +} + +std::unique_ptr MemoryPool::allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + std::lock_guard lock(poolMutex); + + auto [block, startUnit] = findSuitableBlock(poolType, size, alignment); + if (!block) { + return nullptr; + } + + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for alignment) + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; + + // Mark units as used + for (size_t i = startUnit; i < startUnit + requiredUnits; ++i) { + block->freeList[i] = false; + } + + // Create allocation info + auto allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = startUnit * config.allocationUnit; + allocation->size = alignedSize; + allocation->memoryTypeIndex = block->memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->isMapped ? static_cast(block->mappedPtr) + allocation->offset : nullptr; + + block->used += alignedSize; + + return allocation; +} + +void MemoryPool::deallocate(std::unique_ptr allocation) { + if (!allocation) { + return; + } + + std::lock_guard lock(poolMutex); + + // Find the block that contains this allocation + for (auto& [poolType, poolBlocks] : pools) { + const PoolConfig& config = poolConfigs[poolType]; + + for (auto& block : poolBlocks) { + if (*block->memory == allocation->memory) { + // Calculate which units to free + size_t startUnit = allocation->offset / config.allocationUnit; + size_t numUnits = (allocation->size + config.allocationUnit - 1) / config.allocationUnit; + + // Mark units as free + for (size_t i = startUnit; i < startUnit + numUnits; ++i) { + block->freeList[i] = true; + } + + block->used -= allocation->size; + return; + } + } + } + + std::cerr << "Warning: Could not find memory block for deallocation" << std::endl; +} + +std::pair> MemoryPool::createBuffer( + const vk::DeviceSize size, + const vk::BufferUsageFlags usage, + const vk::MemoryPropertyFlags properties) { + // Determine a pool type based on usage and properties + PoolType poolType = PoolType::VERTEX_BUFFER; + + // Check for host-visible requirements first (for instance buffers and staging) + if (properties & vk::MemoryPropertyFlagBits::eHostVisible) { + poolType = PoolType::STAGING_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eVertexBuffer) { + poolType = PoolType::VERTEX_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eIndexBuffer) { + poolType = PoolType::INDEX_BUFFER; + } else if (usage & vk::BufferUsageFlagBits::eUniformBuffer) { + poolType = PoolType::UNIFORM_BUFFER; + } + + // Create the buffer + const vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device, bufferInfo); + + // Get memory requirements + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + // Check if buffer requires device address support (for ray tracing) + const bool needsDeviceAddress = (usage & vk::BufferUsageFlagBits::eShaderDeviceAddress) != vk::BufferUsageFlags{}; + + std::unique_ptr allocation; + + if (needsDeviceAddress) { + // Buffers with device address usage require VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT flag + // Create a dedicated memory block for this buffer (similar to image allocation) + uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + std::lock_guard lock(poolMutex); + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace(poolType).first; + } + auto& poolBlocks = poolIt->second; + auto block = createMemoryBlockWithType(poolType, + memRequirements.size, + memoryTypeIndex, + vk::MemoryAllocateFlagBits::eDeviceAddress); + + // Prepare allocation that uses the new block from offset 0 + allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = 0; + allocation->size = memRequirements.size; + allocation->memoryTypeIndex = memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->mappedPtr; + + // Mark the entire block as used + block->used = memRequirements.size; + const size_t units = block->freeList.size(); + for (size_t i = 0; i < units; ++i) { + block->freeList[i] = false; + } + + // Keep the block owned by the pool for lifetime management + poolBlocks.push_back(std::move(block)); + } else { + // Normal pooled allocation path + allocation = allocate(poolType, memRequirements.size, memRequirements.alignment); + if (!allocation) { + throw std::runtime_error("Failed to allocate memory from pool"); + } + } + + // Bind memory to buffer + buffer.bindMemory(allocation->memory, allocation->offset); + + return {std::move(buffer), std::move(allocation)}; +} + +std::pair> MemoryPool::createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties, + uint32_t mipLevels, + vk::SharingMode sharingMode, + const std::vector& queueFamilyIndices, + uint32_t arrayLayers) { + // Create the image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = std::max(1u, mipLevels), + .arrayLayers = arrayLayers, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = sharingMode, + .initialLayout = vk::ImageLayout::eUndefined + }; + + // If concurrent sharing is requested, provide queue family indices + std::vector fam = queueFamilyIndices; + if (sharingMode == vk::SharingMode::eConcurrent && !fam.empty()) { + imageInfo.queueFamilyIndexCount = static_cast(fam.size()); + imageInfo.pQueueFamilyIndices = fam.data(); + } + + vk::raii::Image image(device, imageInfo); + + // Get memory requirements for this image + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + + // Pick a memory type compatible with this image + uint32_t memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + // Create a dedicated memory block for this image with the exact type and size + std::unique_ptr allocation; { + std::lock_guard lock(poolMutex); + auto poolIt = pools.find(PoolType::TEXTURE_IMAGE); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace(PoolType::TEXTURE_IMAGE).first; + } + auto& poolBlocks = poolIt->second; + auto block = createMemoryBlockWithType(PoolType::TEXTURE_IMAGE, memRequirements.size, memoryTypeIndex); + + // Prepare allocation that uses the new block from offset 0 + allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = 0; + allocation->size = memRequirements.size; + allocation->memoryTypeIndex = memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->mappedPtr; + + // Mark the entire block as used + block->used = memRequirements.size; + const size_t units = block->freeList.size(); + for (size_t i = 0; i < units; ++i) { + block->freeList[i] = false; + } + + // Keep the block owned by the pool for lifetime management and deallocation support + poolBlocks.push_back(std::move(block)); + } + + // Bind memory to image + image.bindMemory(allocation->memory, allocation->offset); + + return {std::move(image), std::move(allocation)}; +} + +std::pair MemoryPool::getMemoryUsage(PoolType poolType) const { + std::lock_guard lock(poolMutex); + + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + return {0, 0}; + } + + auto [used, total] = std::accumulate( + poolIt->second.begin(), + poolIt->second.end(), + std::pair{0, 0}, + [](const auto& acc, const auto& block) { + return std::pair{acc.first + block->used, acc.second + block->size}; + }); + + return {used, total}; +} + +std::pair MemoryPool::getTotalMemoryUsage() const { + std::lock_guard lock(poolMutex); + + vk::DeviceSize totalUsed = 0; + vk::DeviceSize totalAllocated = 0; + + for (const auto& [poolType, poolBlocks] : pools) { + for (const auto& block : poolBlocks) { + totalUsed += block->used; + totalAllocated += block->size; + } + } + + return {totalUsed, totalAllocated}; +} + +bool MemoryPool::preAllocatePools() { + std::lock_guard lock(poolMutex); + + try { + std::cout << "Pre-allocating initial memory blocks for pools..." << std::endl; + + // Pre-allocate at least one block for each pool type + for (const auto& [poolType, config] : poolConfigs) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace(poolType).first; + } + + auto& poolBlocks = poolIt->second; + if (poolBlocks.empty()) { + // Create initial block for this pool type + auto newBlock = createMemoryBlock(poolType, config.blockSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << " Pre-allocated block for pool type " << static_cast(poolType) << std::endl; + } + } + + std::cout << "Memory pool pre-allocation completed successfully" << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to pre-allocate memory pools: " << e.what() << std::endl; + return false; + } +} + +void MemoryPool::setRenderingActive(bool active) { + std::lock_guard lock(poolMutex); + renderingActive = active; +} + +bool MemoryPool::isRenderingActive() const { + std::lock_guard lock(poolMutex); + return renderingActive; +} \ No newline at end of file diff --git a/attachments/openxr_engine/memory_pool.h b/attachments/openxr_engine/memory_pool.h new file mode 100644 index 000000000..04dd1f8cc --- /dev/null +++ b/attachments/openxr_engine/memory_pool.h @@ -0,0 +1,218 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Memory pool allocator for Vulkan resources + * + * This class implements a memory pool system to reduce memory fragmentation + * and improve allocation performance by pre-allocating large chunks of memory + * and sub-allocating from them. + */ +class MemoryPool +{ + public: + /** + * @brief Types of memory pools based on usage patterns + */ + enum class PoolType + { + VERTEX_BUFFER, // Device-local memory for vertex data + INDEX_BUFFER, // Device-local memory for index data + UNIFORM_BUFFER, // Host-visible memory for uniform data + STAGING_BUFFER, // Host-visible memory for staging operations + TEXTURE_IMAGE // Device-local memory for texture images + }; + + /** + * @brief Allocation information for a memory block + */ + struct Allocation + { + vk::DeviceMemory memory; // The underlying device memory + vk::DeviceSize offset; // Offset within the memory block + vk::DeviceSize size; // Size of the allocation + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the memory is persistently mapped + void *mappedPtr; // Mapped pointer (if applicable) + }; + + /** + * @brief Memory block within a pool + */ + struct MemoryBlock + { + vk::raii::DeviceMemory memory; // RAII wrapper for device memory + vk::DeviceSize size; // Total size of the block + vk::DeviceSize used; // Currently used bytes + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the block is mapped + void *mappedPtr; // Mapped pointer (if applicable) + std::vector freeList; // Free list for sub-allocations + vk::DeviceSize allocationUnit; // Size of each allocation unit + }; + + private: + const vk::raii::Device &device; + const vk::raii::PhysicalDevice &physicalDevice; + vk::PhysicalDeviceMemoryProperties memPropsCache{}; + + // Pool configurations + struct PoolConfig + { + vk::DeviceSize blockSize; // Size of each memory block + vk::DeviceSize allocationUnit; // Minimum allocation unit + vk::MemoryPropertyFlags properties; // Memory properties + }; + + // Memory pools for different types + std::unordered_map>> pools; + std::unordered_map poolConfigs; + + // Thread safety + mutable std::mutex poolMutex; + + // Optional rendering state flag (no allocation restrictions enforced) + bool renderingActive = false; + + // Helper methods + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + std::unique_ptr createMemoryBlock(PoolType poolType, vk::DeviceSize size, vk::MemoryAllocateFlags allocFlags = {}); + // Create a memory block with an explicit memory type index (used for images requiring a specific type) + std::unique_ptr createMemoryBlockWithType(PoolType poolType, vk::DeviceSize size, uint32_t memoryTypeIndex, vk::MemoryAllocateFlags allocFlags = {}); + std::pair findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment); + + public: + /** + * @brief Constructor + * @param device Vulkan device + * @param physicalDevice Vulkan physical device + */ + MemoryPool(const vk::raii::Device &device, const vk::raii::PhysicalDevice &physicalDevice); + + /** + * @brief Destructor + */ + ~MemoryPool(); + + /** + * @brief Initialize the memory pool with default configurations + * @return True if initialization was successful + */ + bool initialize(); + + /** + * @brief Allocate memory from a specific pool + * @param poolType Type of pool to allocate from + * @param size Size of the allocation + * @param alignment Required alignment + * @return Allocation information, or nullptr if allocation failed + */ + std::unique_ptr allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment = 1); + + /** + * @brief Free a previously allocated memory block + * @param allocation The allocation to free + */ + void deallocate(std::unique_ptr allocation); + + /** + * @brief Create a buffer using pooled memory + * @param size Size of the buffer + * @param usage Buffer usage flags + * @param properties Memory properties + * @return Pair of buffer and allocation info + */ + std::pair> createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties); + + /** + * @brief Create an image using pooled memory + * @param width Image width + * @param height Image height + * @param format Image format + * @param tiling Image tiling + * @param usage Image usage flags + * @param properties Memory properties + * @return Pair of image and allocation info + */ + std::pair> createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties, + uint32_t mipLevels = 1, + vk::SharingMode sharingMode = vk::SharingMode::eExclusive, + const std::vector &queueFamilyIndices = {}, + uint32_t arrayLayers = 1); + + /** + * @brief Get memory usage statistics + * @param poolType Type of pool to query + * @return Pair of (used bytes, total bytes) + */ + std::pair getMemoryUsage(PoolType poolType) const; + + /** + * @brief Get total memory usage across all pools + * @return Pair of (used bytes, total bytes) + */ + std::pair getTotalMemoryUsage() const; + + /** + * @brief Configure a specific pool type + * @param poolType Type of pool to configure + * @param blockSize Size of each memory block + * @param allocationUnit Minimum allocation unit + * @param properties Memory properties + */ + void configurePool( + PoolType poolType, + vk::DeviceSize blockSize, + vk::DeviceSize allocationUnit, + vk::MemoryPropertyFlags properties); + + /** + * @brief Pre-allocate initial memory blocks for configured pools + * @return True if pre-allocation was successful + */ + bool preAllocatePools(); + + /** + * @brief Set rendering active state flag (informational only) + * @param active Whether rendering is currently active + */ + void setRenderingActive(bool active); + + /** + * @brief Check if rendering is currently active (informational only) + * @return True if rendering is active + */ + bool isRenderingActive() const; +}; diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index a5913c200..ab4763939 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -111,9 +111,19 @@ struct ShadowPushConstants { */ struct UniformBufferObject { alignas(16) glm::mat4 model; - alignas(16) glm::mat4 views[4]; // Supporting up to 4 views (stereo + quad views) - alignas(16) glm::mat4 projs[4]; - alignas(16) glm::vec4 camPoses[4]; + union { + alignas(16) glm::mat4 views[4]; // Supporting up to 4 views (stereo + quad views) + alignas(16) glm::mat4 view; // Single-view alias + }; + union { + alignas(16) glm::mat4 projs[4]; + alignas(16) glm::mat4 proj; // Single-view alias + }; + union { + alignas(16) glm::vec4 camPoses[4]; + alignas(16) glm::vec4 camPos; // Single-view alias + }; + alignas(16) glm::mat4 viewProjections[4]; alignas(4) float exposure; alignas(4) float gamma; alignas(4) float prefilteredCubeMipLevels; @@ -661,7 +671,7 @@ class Renderer { * @param newLayout The new layout. */ void TransitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) { - transitionImageLayout(image, format, oldLayout, newLayout); + transitionImageLayout(image, format, oldLayout, newLayout, 1, 1); } /** @@ -703,12 +713,21 @@ class Renderer { * @brief Get the swap chain image format. * @return The swap chain image format. */ - vk::Format GetSwapChainImageFormat() const { - return swapChainImageFormat; + /** + * @brief Get the OpenXR image views. + * @return Array of two vectors of references to image views (one per eye). + */ + std::array, 2> GetXrImageViews() { + // In this implementaiton, we use a single swapchain with 2 layers for multiview. + // We return raw vk::ImageView to avoid copy/ownership issues with vk::raii::ImageView. + std::vector views; + for (const auto& raiiView : swapChainImageViews) { + views.push_back(*raiiView); + } + return { views, views }; } /** - * @brief Set the framebuffer resized flag. * This should be called when the window is resized to trigger swap chain recreation. */ void SetFramebufferResized() { @@ -1100,6 +1119,7 @@ class Renderer { // Depth buffer vk::raii::Image depthImage = nullptr; std::unique_ptr depthImageAllocation = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; vk::raii::ImageView depthImageView = nullptr; // Forward+ configuration @@ -1880,7 +1900,9 @@ class Renderer { std::pair createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t arrayLayers = 1); std::pair> createImagePooled(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t mipLevels = 1, uint32_t arrayLayers = 1, vk::SharingMode sharingMode = vk::SharingMode::eExclusive, const std::vector& queueFamilies = {}); - void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1, uint32_t layerCount = 1); + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1); + void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels, uint32_t layerCount); + void transitionImageLayout(vk::CommandBuffer cmd, vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1, uint32_t layerCount = 1); void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, vk::ArrayProxy regions); // Extended: track stagedBytes for perf stats void uploadImageFromStaging(vk::Buffer staging, @@ -1890,7 +1912,7 @@ class Renderer { uint32_t mipLevels, vk::DeviceSize stagedBytes); - vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 1, uint32_t layerCount = 1); + vk::raii::ImageView createImageView(vk::Image image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 1, uint32_t layerCount = 1); vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); bool hasStencilComponent(vk::Format format); diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index dc233602c..51c24d344 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -718,9 +718,10 @@ bool Renderer::pickPhysicalDevice() { vk::PhysicalDeviceIDProperties idProps; vk::PhysicalDeviceProperties2 props2; props2.pNext = &idProps; - _device.getProperties2(&props2); + props2 = _device.getProperties2(); - if (std::memcmp(idProps.deviceLUID, xrContext.getRequiredLUID(), VK_LUID_SIZE) != 0) { + const uint8_t* requiredLuid = xrContext.getRequiredLUID(); + if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) { std::cout << " - LUID mismatch for OpenXR" << std::endl; continue; // Not the right GPU for XR! } diff --git a/attachments/openxr_engine/renderer_rendering.cpp b/attachments/openxr_engine/renderer_rendering.cpp index e129d70b4..783fed894 100644 --- a/attachments/openxr_engine/renderer_rendering.cpp +++ b/attachments/openxr_engine/renderer_rendering.cpp @@ -218,7 +218,7 @@ bool Renderer::createReflectionResources(uint32_t width, uint32_t height) { // Allow sampling in glass and blitting to swapchain for diagnostics vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eDeviceLocal, - /*mipLevels*/ + 1, 1, vk::SharingMode::eExclusive, {}); @@ -238,7 +238,7 @@ bool Renderer::createReflectionResources(uint32_t width, uint32_t height) { vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, - /*mipLevels*/ + 1, 1, vk::SharingMode::eExclusive, {}); @@ -503,8 +503,9 @@ bool Renderer::createImageViews() { if (xrMode) { eyeSwapchainImageViews[0].clear(); eyeSwapchainImageViews[1].clear(); - for (auto& image : eyeSwapchainImages[0]) { - eyeSwapchainImageViews[0].emplace_back(createImageView(image, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1, 2)); // 2 layers for multiview + for (size_t i = 0; i < eyeSwapchainImages[0].size(); ++i) { + vk::Image img = eyeSwapchainImages[0][i]; + eyeSwapchainImageViews[0].emplace_back(createImageView(img, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1, 2)); // 2 layers for multiview } return true; } @@ -568,9 +569,9 @@ bool Renderer::setupDynamicRendering() { // Create rendering info renderingInfo = vk::RenderingInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, // 0x3 enables views 0 and 1 for multiview .renderArea = vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent), .layerCount = 1, + .viewMask = xrMode ? 0x3u : 0x0u, // 0x3 enables views 0 and 1 for multiview .colorAttachmentCount = static_cast(colorAttachments.size()), .pColorAttachments = colorAttachments.data(), .pDepthAttachment = &depthAttachment @@ -1151,7 +1152,14 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca EntityResources& entityRes = entityIt->second; ensureEntityMaterialCache(entity, entityRes); - RenderJob job{entity, &entityRes, &meshResources[meshComponent], 0.0f}; + RenderJob job{ + .entity = entity, + .entityRes = &entityRes, + .meshRes = &meshResources[meshComponent], + .meshComp = meshComponent, + .transformComp = entity->GetComponent(), + .isAlphaMasked = entityRes.cachedIsBlended + }; if (entityRes.cachedIsBlended) { transparentJobs.push_back(job); } else { @@ -1172,8 +1180,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca vk::ImageView swapchainView = *eyeSwapchainImageViews[0][imageIndex]; // 2. Transition image to COLOR_ATTACHMENT_OPTIMAL - transitionImageLayout(cmd, swapchainImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, - {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}); // 2 layers + transitionImageLayout(*cmd, swapchainImage, swapChainImageFormat, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, + 1, 2); // 2 layers // 3. Prepare Multiview UBO Template for (uint32_t eye = 0; eye < 2; ++eye) { @@ -1205,9 +1213,9 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca }; vk::RenderingInfo rinfo{ - .viewMask = 0x3u, // Enable view 0 and 1 .renderArea = vk::Rect2D({0, 0}, xrContext.getSwapchainExtent()), .layerCount = 1, + .viewMask = 0x3u, // Enable view 0 and 1 .colorAttachmentCount = 1, .pColorAttachments = &colorAtt, .pDepthAttachment = &depthAtt @@ -1232,8 +1240,8 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca cmd.endRendering(); // 5. Transition back (OpenXR often expects this or TransferSrcOptimal for blitting) - transitionImageLayout(cmd, swapchainImage, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, - {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}); + transitionImageLayout(*cmd, swapchainImage, swapChainImageFormat, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, + 1, 2); // 6. Release OpenXR swapchain image xrContext.releaseSwapchainImage(); diff --git a/attachments/openxr_engine/renderer_resources.cpp b/attachments/openxr_engine/renderer_resources.cpp index 17a28d277..6e5fed666 100644 --- a/attachments/openxr_engine/renderer_resources.cpp +++ b/attachments/openxr_engine/renderer_resources.cpp @@ -434,8 +434,8 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour vk::ImageTiling::eOptimal, usageFlags, vk::MemoryPropertyFlagBits::eDeviceLocal, - /*mipLevels*/ mipLevels, + 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); resources.textureImage = std::move(textureImg); @@ -452,8 +452,8 @@ bool Renderer::createTextureImage(const std::string& texturePath_, TextureResour vk::ImageTiling::eOptimal, usageFlags, vk::MemoryPropertyFlagBits::eDeviceLocal, - /*mipLevels*/ mipLevels, + 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); resources.textureImage = std::move(textureImg2); @@ -589,7 +589,9 @@ bool Renderer::createDefaultTextureResources() { vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal); + vk::MemoryPropertyFlagBits::eDeviceLocal, + 1, + 1); defaultTextureResources.textureImage = std::move(textureImg); defaultTextureResources.textureImageAllocation = std::move(textureImgAllocation); @@ -894,6 +896,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, usageFlags, vk::MemoryPropertyFlagBits::eDeviceLocal, mipLevels, + 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); @@ -911,6 +914,7 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, usageFlags, vk::MemoryPropertyFlagBits::eDeviceLocal, mipLevels, + 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); resources.textureImage = std::move(textureImg); @@ -2180,7 +2184,9 @@ bool Renderer::createOpaqueSceneColorResources() { // Use the same format as the swapchain vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eDeviceLocal); + vk::MemoryPropertyFlagBits::eDeviceLocal, + 1, + 1); opaqueSceneColorImages.push_back(std::move(image)); opaqueSceneColorImageAllocations.push_back(std::move(allocation)); @@ -2314,6 +2320,7 @@ std::pair> Renderer::cr vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, uint32_t mipLevels, + uint32_t arrayLayers, vk::SharingMode sharingMode, const std::vector& queueFamilies) { try { @@ -2330,7 +2337,8 @@ std::pair> Renderer::cr properties, mipLevels, sharingMode, - queueFamilies); + queueFamilies, + arrayLayers); return {std::move(image), std::move(allocation)}; } catch (const std::exception& e) { @@ -2340,12 +2348,12 @@ std::pair> Renderer::cr } // Create an image view -vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels, uint32_t layerCount) { +vk::raii::ImageView Renderer::createImageView(vk::Image image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels, uint32_t layerCount) { try { ensureThreadLocalVulkanInit(); // Create image view vk::ImageViewCreateInfo viewInfo{ - .image = *image, + .image = image, .viewType = (layerCount > 1) ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, .format = format, .subresourceRange = { @@ -2365,6 +2373,10 @@ vk::raii::ImageView Renderer::createImageView(vk::raii::Image& image, vk::Format } // Transition image layout +void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels) { + transitionImageLayout(image, format, oldLayout, newLayout, mipLevels, 1); +} + void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels, uint32_t layerCount) { ensureThreadLocalVulkanInit(); try { @@ -2390,69 +2402,7 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima commandBuffer.begin(beginInfo); - // Create an image barrier (Sync2) - vk::ImageMemoryBarrier2 barrier2{ - .oldLayout = oldLayout, - .newLayout = newLayout, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = image, - .subresourceRange = { - .aspectMask = format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = layerCount - } - }; - - // Set stage and access masks based on layouts - if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; - barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; - barrier2.dstAccessMask = vk::AccessFlagBits2::eTransferWrite; - } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; - barrier2.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; - barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; - barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; - barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead | vk::AccessFlagBits2::eDepthStencilAttachmentWrite; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) { - // Support for shadow map creation: transition from undefined to read-only depth layout - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; - barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; - barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eGeneral) { - // Support for compute shader storage images: transition from undefined to general layout - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; - barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader; - barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderWrite | vk::AccessFlagBits2::eShaderRead; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - // Support for textures that skip staging buffer (e.g., preloaded, generated, or default textures) - // Transition directly from undefined to shader read-only for sampling - barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; - barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; - barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; - barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; - } else { - throw std::invalid_argument("Unsupported layout transition!"); - } - - // Add a barrier to command buffer (Sync2) - vk::DependencyInfo depInfo{ - .dependencyFlags = vk::DependencyFlagBits::eByRegion, - .imageMemoryBarrierCount = 1, - .pImageMemoryBarriers = &barrier2 - }; - commandBuffer.pipelineBarrier2(depInfo); - std::cout << "[transitionImageLayout] recorded barrier image=" << (void *) image << " old=" << static_cast(oldLayout) << " new=" << static_cast(newLayout) << std::endl; + transitionImageLayout(*commandBuffer, image, format, oldLayout, newLayout, mipLevels, layerCount); // End command buffer commandBuffer.end(); @@ -2483,6 +2433,76 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima } // Copy buffer to image +void Renderer::transitionImageLayout(vk::CommandBuffer cmd, vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels, uint32_t layerCount) { + // Create an image barrier (Sync2) + vk::ImageMemoryBarrier2 barrier2{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = format == vk::Format::eD32Sfloat || format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = layerCount + } + }; + + // Set stage and access masks based on layouts + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier2.dstAccessMask = vk::AccessFlagBits2::eTransferWrite; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier2.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; + barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead | vk::AccessFlagBits2::eDepthStencilAttachmentWrite; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests; + barrier2.dstAccessMask = vk::AccessFlagBits2::eDepthStencilAttachmentRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eGeneral) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderWrite | vk::AccessFlagBits2::eShaderRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader; + barrier2.dstAccessMask = vk::AccessFlagBits2::eShaderRead; + } else if (oldLayout == vk::ImageLayout::eColorAttachmentOptimal && newLayout == vk::ImageLayout::eTransferSrcOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; + barrier2.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eTransfer; + barrier2.dstAccessMask = vk::AccessFlagBits2::eTransferRead; + } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eColorAttachmentOptimal) { + barrier2.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe; + barrier2.srcAccessMask = vk::AccessFlagBits2::eNone; + barrier2.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; + barrier2.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite; + } else { + throw std::invalid_argument("Unsupported layout transition!"); + } + + vk::DependencyInfo depInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier2 + }; + cmd.pipelineBarrier2(depInfo); +} + void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, vk::ArrayProxy regions) { ensureThreadLocalVulkanInit(); try { @@ -3199,7 +3219,7 @@ void Renderer::StartUploadsWorker(size_t workerCount) { if (differentFamilies) families = {queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.transferFamily.value()}; vk::Format texFormat = determineTextureFormat(job.idOrPath); - auto [image, imageAlloc] = createImagePooled(job.width, job.height, texFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); + auto [image, imageAlloc] = createImagePooled(job.width, job.height, texFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, 1, 1, differentFamilies ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, families); // Prepare one region std::vector regions{ diff --git a/attachments/openxr_engine/xr_context.cpp b/attachments/openxr_engine/xr_context.cpp index 821ebe486..332427033 100644 --- a/attachments/openxr_engine/xr_context.cpp +++ b/attachments/openxr_engine/xr_context.cpp @@ -11,20 +11,30 @@ #include #endif -XrContext::XrContext() {} +XrContext::XrContext() : + instance(XR_NULL_HANDLE), + systemId(XR_NULL_SYSTEM_ID), + session(XR_NULL_HANDLE), + appSpace(XR_NULL_HANDLE), + actionSet(XR_NULL_HANDLE), + gazeSpace(XR_NULL_HANDLE) +{} XrContext::~XrContext() { cleanup(); } bool XrContext::checkRuntimeAvailable() { - try { - std::vector extensions = xr::enumerateInstanceExtensionProperties(nullptr); - for (const auto& ext : extensions) { - if (std::strcmp(ext.extensionName, XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME) == 0) { - return true; - } - } - } catch (...) { + uint32_t extCount = 0; + if (xrEnumerateInstanceExtensionProperties(nullptr, 0, &extCount, nullptr) != XR_SUCCESS) { + return false; + } + std::vector extensions(extCount, {XR_TYPE_EXTENSION_PROPERTIES}); + if (xrEnumerateInstanceExtensionProperties(nullptr, extCount, &extCount, extensions.data()) != XR_SUCCESS) { return false; } + for (const auto& ext : extensions) { + if (std::strcmp(ext.extensionName, XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME) == 0) { + return true; + } + } return false; } @@ -38,69 +48,73 @@ bool XrContext::isExtensionEnabled(const char* extName) const { bool XrContext::createInstance(const std::string& appName) { std::cout << "XrContext: Creating OpenXR instance for " << appName << std::endl; - try { - // Query available extensions - std::vector availableExtensions = xr::enumerateInstanceExtensionProperties(nullptr); - auto checkExt = [&](const char* name) { - for (const auto& ext : availableExtensions) { - if (std::strcmp(ext.extensionName, name) == 0) return true; - } - return false; - }; - - // 1. Create OpenXR Instance - std::vector extensions = { XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME }; - - // Add optional extensions if available - const char* optionalExtensions[] = { - XR_MSFT_SCENE_UNDERSTANDING_EXTENSION_NAME, - XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME, - XR_MSFT_HAND_INTERACTION_EXTENSION_NAME - }; - - for (auto ext : optionalExtensions) { - if (checkExt(ext)) { - extensions.push_back(ext); - enabledExtensions.push_back(ext); - } + uint32_t extCount = 0; + xrEnumerateInstanceExtensionProperties(nullptr, 0, &extCount, nullptr); + std::vector availableExtensions(extCount, {XR_TYPE_EXTENSION_PROPERTIES}); + xrEnumerateInstanceExtensionProperties(nullptr, extCount, &extCount, availableExtensions.data()); + + auto checkExt = [&](const char* name) { + for (const auto& ext : availableExtensions) { + if (std::strcmp(ext.extensionName, name) == 0) return true; } + return false; + }; + + std::vector extensions = { XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME }; + const char* optionalExtensions[] = { + XR_MSFT_SCENE_UNDERSTANDING_EXTENSION_NAME, + XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME, + XR_MSFT_HAND_INTERACTION_EXTENSION_NAME + }; + + for (auto ext : optionalExtensions) { + if (checkExt(ext)) { + extensions.push_back(ext); + enabledExtensions.push_back(ext); + } + } #if defined(PLATFORM_ANDROID) - if (checkExt(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { - extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); - enabledExtensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); - } + if (checkExt(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { + extensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); + enabledExtensions.push_back(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME); + } #endif - xr::InstanceCreateInfo instanceCreateInfo; - std::strncpy(instanceCreateInfo.applicationInfo.applicationName, appName.c_str(), XR_MAX_APPLICATION_NAME_SIZE); - instanceCreateInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; - instanceCreateInfo.enabledExtensionCount = static_cast(extensions.size()); - instanceCreateInfo.enabledExtensionNames = extensions.data(); + XrInstanceCreateInfo instanceCreateInfo{XR_TYPE_INSTANCE_CREATE_INFO}; + std::strncpy(instanceCreateInfo.applicationInfo.applicationName, appName.c_str(), XR_MAX_APPLICATION_NAME_SIZE); + instanceCreateInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + instanceCreateInfo.enabledExtensionCount = static_cast(extensions.size()); + instanceCreateInfo.enabledExtensionNames = extensions.data(); #if defined(PLATFORM_ANDROID) - if (isExtensionEnabled(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { - if (!androidApp) { - std::cerr << "XrContext: androidApp not set" << std::endl; - return false; - } - - xr::InstanceCreateInfoAndroidKHR androidCreateInfo{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; - androidCreateInfo.applicationVM = androidApp->activity->vm; - androidCreateInfo.applicationActivity = androidApp->activity->clazz; - instanceCreateInfo.next = &androidCreateInfo; + XrInstanceCreateInfoAndroidKHR androidCreateInfo{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; + if (isExtensionEnabled(XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME)) { + if (!androidApp) { + std::cerr << "XrContext: androidApp not set" << std::endl; + return false; } + androidCreateInfo.applicationVM = androidApp->activity->vm; + androidCreateInfo.applicationActivity = androidApp->activity->clazz; + instanceCreateInfo.next = &androidCreateInfo; + } #endif - this->instance = xr::createInstance(instanceCreateInfo); - - // 2. Get System ID - xr::SystemGetInfo systemGetInfo; - systemGetInfo.formFactor = xr::FormFactor::HeadMountedDisplay; - this->systemId = this->instance.getSystem(systemGetInfo); + if (xrCreateInstance(&instanceCreateInfo, &this->instance) != XR_SUCCESS) { + return false; + } - } catch (const std::exception& e) { - std::cerr << "XrContext: Instance initialization failed: " << e.what() << std::endl; + // Load Vulkan extension functions + xrGetInstanceProcAddr(instance, "xrGetVulkanInstanceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanInstanceExtensionsKHR); + xrGetInstanceProcAddr(instance, "xrGetVulkanDeviceExtensionsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanDeviceExtensionsKHR); + xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsRequirementsKHR); + xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsRequirements2KHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsRequirements2KHR); + xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsDeviceKHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsDeviceKHR); + xrGetInstanceProcAddr(instance, "xrGetVulkanGraphicsDevice2KHR", (PFN_xrVoidFunction*)&pfnGetVulkanGraphicsDevice2KHR); + + XrSystemGetInfo systemGetInfo{XR_TYPE_SYSTEM_GET_INFO}; + systemGetInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + if (xrGetSystem(this->instance, &systemGetInfo, &this->systemId) != XR_SUCCESS) { return false; } @@ -110,116 +124,100 @@ bool XrContext::createInstance(const std::string& appName) { bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device device, uint32_t queueFamilyIndex, uint32_t queueIndex) { std::cout << "XrContext: Creating session" << std::endl; - try { - // 4. Create Session - XrGraphicsBindingVulkanKHR graphicsBinding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR}; - graphicsBinding.instance = (VkInstance)this->vkInstance; - graphicsBinding.physicalDevice = (VkPhysicalDevice)physicalDevice; - graphicsBinding.device = (VkDevice)device; - graphicsBinding.queueFamilyIndex = queueFamilyIndex; - graphicsBinding.queueIndex = queueIndex; - - xr::SessionCreateInfo sessionCreateInfo; - sessionCreateInfo.next = &graphicsBinding; - sessionCreateInfo.systemId = this->systemId; - this->session = this->instance.createSession(sessionCreateInfo); - - // 5. Create Reference Space - xr::ReferenceSpaceCreateInfo spaceCreateInfo; - spaceCreateInfo.referenceSpaceType = xr::ReferenceSpaceType::Stage; - spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; - this->appSpace = this->session.createReferenceSpace(spaceCreateInfo); - - // Initialize views with identity/default values to avoid out-of-bounds access - this->views.resize(2); - for (uint32_t i = 0; i < 2; ++i) { - this->views[i].pose = {{0,0,0,1}, {0,0,0}}; - this->views[i].fov = {-1, 1, 1, -1}; - } + XrGraphicsBindingVulkanKHR graphicsBinding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR}; + graphicsBinding.instance = (VkInstance)this->vkInstance; + graphicsBinding.physicalDevice = (VkPhysicalDevice)physicalDevice; + graphicsBinding.device = (VkDevice)device; + graphicsBinding.queueFamilyIndex = queueFamilyIndex; + graphicsBinding.queueIndex = queueIndex; + + XrSessionCreateInfo sessionCreateInfo{XR_TYPE_SESSION_CREATE_INFO}; + sessionCreateInfo.next = &graphicsBinding; + sessionCreateInfo.systemId = this->systemId; + if (xrCreateSession(this->instance, &sessionCreateInfo, &this->session) != XR_SUCCESS) { + return false; + } - // 6. Initialize Action System (Chapter 7) - xr::ActionSetCreateInfo actionSetInfo; - std::strncpy(actionSetInfo.actionSetName, "main", XR_MAX_ACTION_SET_NAME_SIZE); - std::strncpy(actionSetInfo.localizedActionSetName, "Main Actions", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE); - this->actionSet = this->instance.createActionSet(actionSetInfo); - - auto createAction = [&](const std::string& name, const std::string& localizedName, xr::ActionType type) { - xr::ActionCreateInfo actionInfo; - actionInfo.actionType = type; - std::strncpy(actionInfo.actionName, name.c_str(), XR_MAX_ACTION_NAME_SIZE); - std::strncpy(actionInfo.localizedActionName, localizedName.c_str(), XR_MAX_LOCALIZED_ACTION_NAME_SIZE); - this->actions[name] = this->actionSet.createAction(actionInfo); - this->actionTypes[name] = type; - }; - - createAction("trigger_left", "Left Trigger", xr::ActionType::BooleanInput); - createAction("trigger_right", "Right Trigger", xr::ActionType::BooleanInput); - createAction("pose_left", "Left Hand Pose", xr::ActionType::PoseInput); - createAction("pose_right", "Right Hand Pose", xr::ActionType::PoseInput); - createAction("grab_left", "Left Grab", xr::ActionType::FloatInput); - createAction("grab_right", "Right Grab", xr::ActionType::FloatInput); - createAction("Grab", "Grab", xr::ActionType::BooleanInput); - createAction("GrabPose", "Grab Pose", xr::ActionType::PoseInput); - createAction("menu", "Menu Button", xr::ActionType::BooleanInput); - - // Suggest bindings for simple controller - xr::Path khrSimplePath = this->instance.stringToPath("/interaction_profiles/khr/simple_controller"); - std::vector bindings = { - {actions["trigger_left"], this->instance.stringToPath("/user/hand/left/input/select/click")}, - {actions["trigger_right"], this->instance.stringToPath("/user/hand/right/input/select/click")}, - {actions["pose_left"], this->instance.stringToPath("/user/hand/left/input/grip/pose")}, - {actions["pose_right"], this->instance.stringToPath("/user/hand/right/input/grip/pose")}, - {actions["Grab"], this->instance.stringToPath("/user/hand/right/input/select/click")}, - {actions["GrabPose"], this->instance.stringToPath("/user/hand/right/input/grip/pose")}, - {actions["menu"], this->instance.stringToPath("/user/hand/left/input/menu/click")} - }; - xr::InteractionProfileSuggestedBinding suggestedBindings; - suggestedBindings.interactionProfile = khrSimplePath; - suggestedBindings.suggestedBindings = bindings.data(); - suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); - this->instance.suggestInteractionProfileBindings(suggestedBindings); - - xr::SessionActionSetsAttachInfo attachInfo; - attachInfo.countActionSets = 1; - attachInfo.actionSets = &this->actionSet; - this->session.attachSessionActionSets(attachInfo); - - // Create spaces for pose actions - for (const auto& actionName : {"pose_left", "pose_right", "GrabPose"}) { - xr::ActionSpaceCreateInfo actionSpaceInfo; - actionSpaceInfo.action = actions[actionName]; - actionSpaceInfo.poseInActionSpace = {{0,0,0,1}, {0,0,0}}; - this->actionSpaces[actionName] = this->session.createActionSpace(actionSpaceInfo); - } + XrReferenceSpaceCreateInfo spaceCreateInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + if (xrCreateReferenceSpace(this->session, &spaceCreateInfo, &this->appSpace) != XR_SUCCESS) { + return false; + } - // 7. Initialize Eye Gaze (Chapter 17) - if (isExtensionEnabled(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME)) { - xr::ReferenceSpaceCreateInfo gazeSpaceInfo; - gazeSpaceInfo.referenceSpaceType = (xr::ReferenceSpaceType)XR_REFERENCE_SPACE_TYPE_EYE_GAZE_EXT; - gazeSpaceInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; - try { - this->gazeSpace = this->session.createReferenceSpace(gazeSpaceInfo); - } catch (const std::exception& e) { - std::cout << "XrContext: Eye gaze space creation failed: " << e.what() << std::endl; - } - } + this->views.resize(2, {XR_TYPE_VIEW}); + for (uint32_t i = 0; i < 2; ++i) { + this->views[i].pose = {{0,0,0,1}, {0,0,0}}; + this->views[i].fov = {-1, 1, 1, -1}; + } - // 8. Initialize Scene Understanding (Chapter 16) - if (isExtensionEnabled(XR_MSFT_SCENE_UNDERSTANDING_EXTENSION_NAME)) { - PFN_xrCreateSceneObserverMSFT xrCreateSceneObserverMSFT_ptr; - this->instance.getInstanceProcAddr("xrCreateSceneObserverMSFT", (PFN_xrVoidFunction*)&xrCreateSceneObserverMSFT_ptr); - if (xrCreateSceneObserverMSFT_ptr) { - XrSceneObserverCreateInfoMSFT createInfo{XR_TYPE_SCENE_OBSERVER_CREATE_INFO_MSFT}; - XrSceneObserverMSFT observer; - if (xrCreateSceneObserverMSFT_ptr((XrSession)this->session, &createInfo, &observer) == XR_SUCCESS) { - this->sceneObserver = xr::SceneObserverMSFT(observer); - } - } - } + XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; + std::strncpy(actionSetInfo.actionSetName, "main", XR_MAX_ACTION_SET_NAME_SIZE); + std::strncpy(actionSetInfo.localizedActionSetName, "Main Actions", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE); + xrCreateActionSet(this->instance, &actionSetInfo, &this->actionSet); + + auto createAction = [&](const std::string& name, const std::string& localizedName, XrActionType type) { + XrActionCreateInfo actionInfo{XR_TYPE_ACTION_CREATE_INFO}; + actionInfo.actionType = type; + std::strncpy(actionInfo.actionName, name.c_str(), XR_MAX_ACTION_NAME_SIZE); + std::strncpy(actionInfo.localizedActionName, localizedName.c_str(), XR_MAX_LOCALIZED_ACTION_NAME_SIZE); + XrAction action; + xrCreateAction(this->actionSet, &actionInfo, &action); + this->actions[name] = action; + this->actionTypes[name] = type; + }; + + createAction("trigger_left", "Left Trigger", XR_ACTION_TYPE_BOOLEAN_INPUT); + createAction("trigger_right", "Right Trigger", XR_ACTION_TYPE_BOOLEAN_INPUT); + createAction("pose_left", "Left Hand Pose", XR_ACTION_TYPE_POSE_INPUT); + createAction("pose_right", "Right Hand Pose", XR_ACTION_TYPE_POSE_INPUT); + createAction("grab_left", "Left Grab", XR_ACTION_TYPE_FLOAT_INPUT); + createAction("grab_right", "Right Grab", XR_ACTION_TYPE_FLOAT_INPUT); + createAction("Grab", "Grab", XR_ACTION_TYPE_BOOLEAN_INPUT); + createAction("GrabPose", "Grab Pose", XR_ACTION_TYPE_POSE_INPUT); + createAction("menu", "Menu Button", XR_ACTION_TYPE_BOOLEAN_INPUT); + + XrPath khrSimplePath; + xrStringToPath(this->instance, "/interaction_profiles/khr/simple_controller", &khrSimplePath); + std::vector bindings; + auto addBinding = [&](const std::string& act, const char* path) { + XrPath p; + xrStringToPath(this->instance, path, &p); + bindings.push_back({actions[act], p}); + }; + addBinding("trigger_left", "/user/hand/left/input/select/click"); + addBinding("trigger_right", "/user/hand/right/input/select/click"); + addBinding("pose_left", "/user/hand/left/input/grip/pose"); + addBinding("pose_right", "/user/hand/right/input/grip/pose"); + addBinding("Grab", "/user/hand/right/input/select/click"); + addBinding("GrabPose", "/user/hand/right/input/grip/pose"); + addBinding("menu", "/user/hand/left/input/menu/click"); + + XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; + suggestedBindings.interactionProfile = khrSimplePath; + suggestedBindings.suggestedBindings = bindings.data(); + suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); + xrSuggestInteractionProfileBindings(this->instance, &suggestedBindings); + + XrSessionActionSetsAttachInfo attachInfo{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO}; + attachInfo.countActionSets = 1; + attachInfo.actionSets = &this->actionSet; + xrAttachSessionActionSets(this->session, &attachInfo); + + for (const auto& actionName : {"pose_left", "pose_right", "GrabPose"}) { + XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; + actionSpaceInfo.action = actions[actionName]; + actionSpaceInfo.poseInActionSpace = {{0,0,0,1}, {0,0,0}}; + XrSpace space; + xrCreateActionSpace(this->session, &actionSpaceInfo, &space); + this->actionSpaces[actionName] = space; + } - } catch (const std::exception& e) { - std::cerr << "XrContext: Session creation failed: " << e.what() << std::endl; - return false; + if (isExtensionEnabled(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME)) { + XrReferenceSpaceCreateInfo gazeSpaceInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + gazeSpaceInfo.referenceSpaceType = (XrReferenceSpaceType)1000031008; // XR_REFERENCE_SPACE_TYPE_EYE_GAZE_EXT + gazeSpaceInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + xrCreateReferenceSpace(this->session, &gazeSpaceInfo, &this->gazeSpace); } return true; @@ -227,141 +225,172 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi void XrContext::cleanup() { for (auto& swapchain : swapchains) { - swapchain.handle.destroy(); + xrDestroySwapchain(swapchain.handle); } swapchains.clear(); - if (sceneObserver) { - PFN_xrDestroySceneObserverMSFT xrDestroySceneObserverMSFT_ptr; - this->instance.getInstanceProcAddr("xrDestroySceneObserverMSFT", (PFN_xrVoidFunction*)&xrDestroySceneObserverMSFT_ptr); - if (xrDestroySceneObserverMSFT_ptr) { - xrDestroySceneObserverMSFT_ptr((XrSceneObserverMSFT)sceneObserver); - } - sceneObserver = nullptr; - } - - if (gazeSpace) { - gazeSpace.destroy(); - gazeSpace = nullptr; + if (gazeSpace != XR_NULL_HANDLE) { + xrDestroySpace(gazeSpace); + gazeSpace = XR_NULL_HANDLE; } for (auto& pair : actionSpaces) { - pair.second.destroy(); + xrDestroySpace(pair.second); } actionSpaces.clear(); for (auto& pair : actions) { - pair.second.destroy(); + xrDestroyAction(pair.second); } actions.clear(); - if (actionSet) { - actionSet.destroy(); - actionSet = nullptr; + if (actionSet != XR_NULL_HANDLE) { + xrDestroyActionSet(actionSet); + actionSet = XR_NULL_HANDLE; } - if (appSpace) { - appSpace.destroy(); - appSpace = nullptr; + if (appSpace != XR_NULL_HANDLE) { + xrDestroySpace(appSpace); + appSpace = XR_NULL_HANDLE; } - if (session) { - session.destroy(); - session = nullptr; + if (session != XR_NULL_HANDLE) { + xrDestroySession(session); + session = XR_NULL_HANDLE; } - if (instance) { - instance.destroy(); - instance = nullptr; + if (instance != XR_NULL_HANDLE) { + xrDestroyInstance(instance); + instance = XR_NULL_HANDLE; } } std::vector XrContext::getVulkanInstanceExtensions() { - if (!instance) { - // Fallback for when instance isn't created yet (though it should be) - return { "XR_KHR_vulkan_enable2" }; - } - - // Use the real API to query required extensions - std::string extensions = this->instance.getVulkanInstanceExtensionsKHR(this->systemId); - - // Parse the space-separated string into a vector of const char* - static std::vector extList; - extList.clear(); - std::string ext; + if (instance == XR_NULL_HANDLE) return { "XR_KHR_vulkan_enable2" }; + uint32_t size = 0; + if (!pfnGetVulkanInstanceExtensionsKHR) return {}; + pfnGetVulkanInstanceExtensionsKHR(instance, systemId, 0, &size, nullptr); + std::vector buffer(size); + pfnGetVulkanInstanceExtensionsKHR(instance, systemId, size, &size, buffer.data()); + + static std::vector extStrings; + extStrings.clear(); + std::string extensions(buffer.data()); std::istringstream iss(extensions); - while (iss >> ext) { - extList.push_back(ext); - } + std::string ext; + while (iss >> ext) extStrings.push_back(ext); static std::vector extPtrs; extPtrs.clear(); - for (const auto& s : extList) { - extPtrs.push_back(s.c_str()); - } - + for (const auto& s : extStrings) extPtrs.push_back(s.c_str()); return extPtrs; } std::vector XrContext::getVulkanDeviceExtensions(vk::PhysicalDevice physicalDevice) { - if (!instance) { - return { "VK_KHR_external_memory", "VK_KHR_external_semaphore" }; - } - - std::string extensions = this->instance.getVulkanDeviceExtensionsKHR(this->systemId); - - static std::vector devExtList; - devExtList.clear(); - std::string ext; + if (instance == XR_NULL_HANDLE) return { "VK_KHR_external_memory", "VK_KHR_external_semaphore" }; + uint32_t size = 0; + if (!pfnGetVulkanDeviceExtensionsKHR) return {}; + pfnGetVulkanDeviceExtensionsKHR(instance, systemId, 0, &size, nullptr); + std::vector buffer(size); + pfnGetVulkanDeviceExtensionsKHR(instance, systemId, size, &size, buffer.data()); + + static std::vector devExtStrings; + devExtStrings.clear(); + std::string extensions(buffer.data()); std::istringstream iss(extensions); - while (iss >> ext) { - devExtList.push_back(ext); - } + std::string ext; + while (iss >> ext) devExtStrings.push_back(ext); static std::vector devExtPtrs; devExtPtrs.clear(); - for (const auto& s : devExtList) { - devExtPtrs.push_back(s.c_str()); - } - + for (const auto& s : devExtStrings) devExtPtrs.push_back(s.c_str()); return devExtPtrs; } const uint8_t* XrContext::getRequiredLUID() { - static uint8_t luid[8] = {0}; - xr::GraphicsRequirementsVulkanKHR graphicsRequirements; - this->instance.getVulkanGraphicsRequirements2KHR(this->systemId, &graphicsRequirements); - std::memcpy(luid, &graphicsRequirements.graphicsDeviceLuid, 8); - return luid; + if (!luidValid && vkInstance && instance != XR_NULL_HANDLE && systemId != XR_NULL_SYSTEM_ID) { + // Step 1: Call graphics requirements as mandated by spec before getting graphics device + XrGraphicsRequirementsVulkanKHR graphicsRequirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR}; + if (pfnGetVulkanGraphicsRequirements2KHR) { + pfnGetVulkanGraphicsRequirements2KHR(instance, systemId, &graphicsRequirements); + } else if (pfnGetVulkanGraphicsRequirementsKHR) { + pfnGetVulkanGraphicsRequirementsKHR(instance, systemId, &graphicsRequirements); + } + + // Step 2: Get the physical device from OpenXR + VkPhysicalDevice vkPhysicalDevice = VK_NULL_HANDLE; + XrResult result = XR_ERROR_FUNCTION_UNSUPPORTED; + + if (pfnGetVulkanGraphicsDevice2KHR) { + XrVulkanGraphicsDeviceGetInfoKHR getInfo{XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR}; + getInfo.systemId = systemId; + getInfo.vulkanInstance = (VkInstance)vkInstance; + result = pfnGetVulkanGraphicsDevice2KHR(instance, &getInfo, &vkPhysicalDevice); + } else if (pfnGetVulkanGraphicsDeviceKHR) { + result = pfnGetVulkanGraphicsDeviceKHR(instance, systemId, (VkInstance)vkInstance, &vkPhysicalDevice); + } + + if (result == XR_SUCCESS && vkPhysicalDevice != VK_NULL_HANDLE) { + // Step 3: Extract LUID from the physical device + VkPhysicalDeviceIDProperties idProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES}; + idProps.pNext = nullptr; + VkPhysicalDeviceProperties2 props2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; + props2.pNext = &idProps; + + auto pfnGetPhysicalDeviceProperties2 = (PFN_vkGetPhysicalDeviceProperties2)vkGetInstanceProcAddr((VkInstance)vkInstance, "vkGetPhysicalDeviceProperties2"); + if (pfnGetPhysicalDeviceProperties2) { + pfnGetPhysicalDeviceProperties2(vkPhysicalDevice, &props2); + if (idProps.deviceLUIDValid) { + std::memcpy(requiredLuid, idProps.deviceLUID, VK_LUID_SIZE); + luidValid = true; + std::cout << "XrContext: Required LUID found and stored." << std::endl; + } else { + std::cout << "XrContext: Physical device LUID is not valid." << std::endl; + } + } else { + std::cerr << "XrContext: Failed to load vkGetPhysicalDeviceProperties2" << std::endl; + } + } else { + std::cerr << "XrContext: Failed to get Vulkan graphics device from OpenXR (XrResult=" << result << ")" << std::endl; + } + } + return luidValid ? requiredLuid : nullptr; } vk::Extent2D XrContext::getRecommendedExtent() const { - uint32_t viewCount = 0; - std::vector views = this->instance.enumerateViewConfigurationViewsToVector(this->systemId, xr::ViewConfigurationType::PrimaryStereo); - if (views.empty()) return {1024, 1024}; - return {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight}; + uint32_t count = 0; + xrEnumerateViewConfigurationViews(instance, systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &count, nullptr); + std::vector vcv(count, {XR_TYPE_VIEW_CONFIGURATION_VIEW}); + xrEnumerateViewConfigurationViews(instance, systemId, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, count, &count, vcv.data()); + if (vcv.empty()) return {1024, 1024}; + vk::Extent2D ext{vcv[0].recommendedImageRectWidth, vcv[0].recommendedImageRectHeight}; + return ext; } void XrContext::createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent) { this->format = format; this->extent = extent; - xr::SwapchainCreateInfo swapchainCreateInfo; - swapchainCreateInfo.arrayCount = 2; // Multiview (Layered) - swapchainCreateInfo.format = (int64_t)format; - swapchainCreateInfo.width = extent.width; - swapchainCreateInfo.height = extent.height; - swapchainCreateInfo.mipCount = 1; - swapchainCreateInfo.faceCount = 1; - swapchainCreateInfo.sampleCount = 1; - swapchainCreateInfo.usageFlags = xr::SwapchainUsageFlagBits::ColorAttachment; + XrSwapchainCreateInfo ci{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + ci.arraySize = 2; + ci.format = (int64_t)format; + ci.width = extent.width; + ci.height = extent.height; + ci.mipCount = 1; + ci.faceCount = 1; + ci.sampleCount = 1; + ci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; - xr::Swapchain swapchainHandle = this->session.createSwapchain(swapchainCreateInfo); + XrSwapchain handle; + xrCreateSwapchain(this->session, &ci, &handle); - std::vector images = swapchainHandle.enumerateSwapchainImagesToVector(); + uint32_t count = 0; + xrEnumerateSwapchainImages(handle, 0, &count, nullptr); + std::vector images(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); + xrEnumerateSwapchainImages(handle, count, &count, (XrSwapchainImageBaseHeader*)images.data()); SwapchainData data; - data.handle = swapchainHandle; + data.handle = handle; data.images = std::move(images); this->swapchains.push_back(std::move(data)); } @@ -370,7 +399,7 @@ std::vector XrContext::enumerateSwapchainImages() { std::vector vkImages; if (!swapchains.empty()) { for (const auto& img : swapchains[0].images) { - vkImages.push_back((VkImage)img.image); + vkImages.push_back(vk::Image(img.image)); } } return vkImages; @@ -378,44 +407,48 @@ std::vector XrContext::enumerateSwapchainImages() { void XrContext::waitSwapchainImage() { if (swapchains.empty()) return; - xr::SwapchainWaitInfo waitInfo; - waitInfo.timeout = xr::Duration::infinite(); - swapchains[0].handle.waitSwapchainImage(waitInfo); + XrSwapchainImageWaitInfo wi{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + wi.timeout = XR_INFINITE_DURATION; + xrWaitSwapchainImage(swapchains[0].handle, &wi); } uint32_t XrContext::acquireSwapchainImage() { if (swapchains.empty()) return 0; - xr::SwapchainAcquireInfo acquireInfo; - return swapchains[0].handle.acquireSwapchainImage(acquireInfo); + XrSwapchainImageAcquireInfo ai{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t index = 0; + xrAcquireSwapchainImage(swapchains[0].handle, &ai, &index); + return index; } void XrContext::releaseSwapchainImage() { if (swapchains.empty()) return; - xr::SwapchainReleaseInfo releaseInfo; - swapchains[0].handle.releaseSwapchainImage(releaseInfo); + XrSwapchainImageReleaseInfo ri{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + xrReleaseSwapchainImage(swapchains[0].handle, &ri); } XrFrameState XrContext::waitFrame() { - xr::FrameWaitInfo waitInfo; - this->frameState = this->session.waitFrame(waitInfo); - return (XrFrameState)this->frameState; + XrFrameWaitInfo wi{XR_TYPE_FRAME_WAIT_INFO}; + this->frameState = {XR_TYPE_FRAME_STATE}; + xrWaitFrame(this->session, &wi, &this->frameState); + return this->frameState; } void XrContext::beginFrame() { - xr::FrameBeginInfo beginInfo; - this->session.beginFrame(beginInfo); + XrFrameBeginInfo bi{XR_TYPE_FRAME_BEGIN_INFO}; + xrBeginFrame(this->session, &bi); } -void XrContext::endFrame(const std::array, 2>& eyeViews) { - xr::FrameEndInfo endInfo; - endInfo.displayTime = this->frameState.predictedDisplayTime; - endInfo.environmentBlendMode = xr::EnvironmentBlendMode::Opaque; +void XrContext::endFrame(const std::array, 2>& eyeViews) { + XrFrameEndInfo ei{XR_TYPE_FRAME_END_INFO}; + ei.displayTime = this->frameState.predictedDisplayTime; + ei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; - xr::CompositionLayerProjection layer; + XrCompositionLayerProjection layer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; layer.space = this->appSpace; - std::vector projectionViews(2); + static XrCompositionLayerProjectionView projectionViews[2]; for (uint32_t i = 0; i < 2; ++i) { + projectionViews[i] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}; projectionViews[i].pose = this->views[i].pose; projectionViews[i].fov = this->views[i].fov; projectionViews[i].subImage.swapchain = this->swapchains[0].handle; @@ -423,28 +456,30 @@ void XrContext::endFrame(const std::array, 2>& projectionViews[i].subImage.imageArrayIndex = i; } - layer.viewCount = (uint32_t)projectionViews.size(); - layer.views = projectionViews.data(); + layer.viewCount = 2; + layer.views = projectionViews; - std::vector layers; + static const XrCompositionLayerBaseHeader* layers[1]; + uint32_t layerCount = 0; if (this->frameState.shouldRender) { - layers.push_back(reinterpret_cast(&layer)); + layers[layerCount++] = (XrCompositionLayerBaseHeader*)&layer; } - endInfo.layerCount = (uint32_t)layers.size(); - endInfo.layers = layers.data(); + ei.layerCount = layerCount; + ei.layers = layers; - this->session.endFrame(endInfo); + xrEndFrame(this->session, &ei); } void XrContext::locateViews(XrTime predictedTime) { - xr::ViewLocateInfo locateInfo; - locateInfo.viewConfigurationType = xr::ViewConfigurationType::PrimaryStereo; - locateInfo.displayTime = predictedTime; - locateInfo.space = this->appSpace; - - auto [result, viewState, locatedViews] = this->session.locateViewsToVector(locateInfo); - this->views = std::move(locatedViews); + XrViewLocateInfo li{XR_TYPE_VIEW_LOCATE_INFO}; + li.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + li.displayTime = predictedTime; + li.space = this->appSpace; + + XrViewState vs{XR_TYPE_VIEW_STATE}; + uint32_t count = 0; + xrLocateViews(this->session, &li, &vs, (uint32_t)this->views.size(), &count, this->views.data()); } vk::Viewport XrContext::getViewport(uint32_t eye) const { @@ -457,19 +492,11 @@ vk::Rect2D XrContext::getScissor(uint32_t eye) const { glm::mat4 XrContext::getProjectionMatrix(uint32_t eye) const { if (eye >= views.size()) return glm::mat4(1.0f); - const auto& fov = views[eye].fov; - float nearZ = 0.1f; - float farZ = 100.0f; - - float tanLeft = std::tan(fov.angleLeft); - float tanRight = std::tan(fov.angleRight); - float tanDown = std::tan(fov.angleDown); - float tanUp = std::tan(fov.angleUp); - - float tanWidth = tanRight - tanLeft; - float tanHeight = tanUp - tanDown; - + float nearZ = 0.1f, farZ = 100.0f; + float tanLeft = std::tan(fov.angleLeft), tanRight = std::tan(fov.angleRight); + float tanDown = std::tan(fov.angleDown), tanUp = std::tan(fov.angleUp); + float tanWidth = tanRight - tanLeft, tanHeight = tanUp - tanDown; glm::mat4 projection = glm::mat4(0.0f); projection[0][0] = 2.0f / tanWidth; projection[1][1] = 2.0f / tanHeight; @@ -478,7 +505,6 @@ glm::mat4 XrContext::getProjectionMatrix(uint32_t eye) const { projection[2][2] = -farZ / (farZ - nearZ); projection[2][3] = -1.0f; projection[3][2] = -(farZ * nearZ) / (farZ - nearZ); - return projection; } @@ -493,202 +519,63 @@ glm::vec3 XrContext::getEyePosition(uint32_t eye) const { } void XrContext::pollActions() { - if (!session || !actionSet) return; - - xr::ActionsSyncInfo syncInfo; - xr::ActiveActionSet activeSet{actionSet, xr::Space(nullptr)}; - syncInfo.activeActionSets = &activeSet; - syncInfo.countActiveActionSets = 1; - - if (session.syncActions(syncInfo) != xr::Result::Success) { - return; - } + if (session == XR_NULL_HANDLE || actionSet == XR_NULL_HANDLE) return; + XrActionsSyncInfo si{XR_TYPE_ACTIONS_SYNC_INFO}; + static XrActiveActionSet as; + as.actionSet = actionSet; + as.subactionPath = XR_NULL_PATH; + si.activeActionSets = &as; + si.countActiveActionSets = 1; + xrSyncActions(session, &si); } bool XrContext::isActionActive(const std::string& name) const { - if (!session || actions.find(name) == actions.end()) return false; - - xr::Action action = actions.at(name); - xr::ActionType type = actionTypes.at(name); - xr::ActionStateGetInfo getInfo{action}; - - if (type == xr::ActionType::BooleanInput) { - auto state = session.getActionStateBoolean(getInfo); - if (state.isActive) return state.currentState; - } else if (type == xr::ActionType::FloatInput) { - auto state = session.getActionStateFloat(getInfo); - if (state.isActive) return state.currentState > 0.1f; + if (session == XR_NULL_HANDLE || actions.find(name) == actions.end()) return false; + XrAction action = actions.at(name); + XrActionType type = actionTypes.at(name); + XrActionStateGetInfo gi{XR_TYPE_ACTION_STATE_GET_INFO}; + gi.action = action; + if (type == XR_ACTION_TYPE_BOOLEAN_INPUT) { + XrActionStateBoolean st{XR_TYPE_ACTION_STATE_BOOLEAN}; + xrGetActionStateBoolean(session, &gi, &st); + if (st.isActive) return st.currentState; + } else if (type == XR_ACTION_TYPE_FLOAT_INPUT) { + XrActionStateFloat st{XR_TYPE_ACTION_STATE_FLOAT}; + xrGetActionStateFloat(session, &gi, &st); + if (st.isActive) return st.currentState > 0.1f; } - return false; } XrPosef XrContext::getActionPose(const std::string& name) const { - if (!session || actionSpaces.find(name) == actionSpaces.end()) { - XrPosef pose; - pose.orientation = {0,0,0,1}; - pose.position = {0,0,0}; - return pose; + if (session == XR_NULL_HANDLE || actionSpaces.find(name) == actionSpaces.end()) { + return {{0,0,0,1}, {0,0,0}}; } - - xr::Space space = actionSpaces.at(name); - auto location = space.locateSpace(appSpace, frameState.predictedDisplayTime); - - if (location.locationFlags & xr::SpaceLocationFlagBits::OrientationValid && - location.locationFlags & xr::SpaceLocationFlagBits::PositionValid) { - return (XrPosef)location.pose; + XrAction action = actions.at(name); + XrSpace space = actionSpaces.at(name); + XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION}; + xrLocateSpace(space, appSpace, frameState.predictedDisplayTime, &loc); + if ((loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) && (loc.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT)) { + return loc.pose; } - - XrPosef pose; - pose.orientation = {0,0,0,1}; - pose.position = {0,0,0}; - return pose; + return {{0,0,0,1}, {0,0,0}}; } std::vector XrContext::getLatestSpatialMeshes() { - if (!sceneObserver) return {}; - - std::vector meshes; - - // Use extension function pointers - PFN_xrComputeNewSceneMSFT xrComputeNewSceneMSFT_ptr; - PFN_xrGetSceneComputeStateMSFT xrGetSceneComputeStateMSFT_ptr; - PFN_xrCreateSceneMSFT xrCreateSceneMSFT_ptr; - PFN_xrGetSceneComponentsMSFT xrGetSceneComponentsMSFT_ptr; - PFN_xrGetSceneMeshBuffersMSFT xrGetSceneMeshBuffersMSFT_ptr; - PFN_xrGetSceneComponentLocationsMSFT xrGetSceneComponentLocationsMSFT_ptr; - PFN_xrDestroySceneMSFT xrDestroySceneMSFT_ptr; - - this->instance.getInstanceProcAddr("xrComputeNewSceneMSFT", (PFN_xrVoidFunction*)&xrComputeNewSceneMSFT_ptr); - this->instance.getInstanceProcAddr("xrGetSceneComputeStateMSFT", (PFN_xrVoidFunction*)&xrGetSceneComputeStateMSFT_ptr); - this->instance.getInstanceProcAddr("xrCreateSceneMSFT", (PFN_xrVoidFunction*)&xrCreateSceneMSFT_ptr); - this->instance.getInstanceProcAddr("xrGetSceneComponentsMSFT", (PFN_xrVoidFunction*)&xrGetSceneComponentsMSFT_ptr); - this->instance.getInstanceProcAddr("xrGetSceneMeshBuffersMSFT", (PFN_xrVoidFunction*)&xrGetSceneMeshBuffersMSFT_ptr); - this->instance.getInstanceProcAddr("xrGetSceneComponentLocationsMSFT", (PFN_xrVoidFunction*)&xrGetSceneComponentLocationsMSFT_ptr); - this->instance.getInstanceProcAddr("xrDestroySceneMSFT", (PFN_xrVoidFunction*)&xrDestroySceneMSFT_ptr); - - if (!xrComputeNewSceneMSFT_ptr || !xrGetSceneComputeStateMSFT_ptr || !xrCreateSceneMSFT_ptr || - !xrGetSceneComponentsMSFT_ptr || !xrGetSceneMeshBuffersMSFT_ptr || !xrGetSceneComponentLocationsMSFT_ptr || !xrDestroySceneMSFT_ptr) { - return {}; - } - - // 1. Check if we need to start a new computation or if one is ready - XrSceneComputeStateMSFT state; - xrGetSceneComputeStateMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &state); - - if (state == XR_SCENE_COMPUTE_STATE_NONE_MSFT) { - XrSceneComputeInfoMSFT computeInfo{XR_TYPE_SCENE_COMPUTE_INFO_MSFT}; - computeInfo.consistency = XR_SCENE_COMPUTE_CONSISTENCY_SNAPSHOT_MSFT; - XrSceneSphereBoundMSFT sphereBound; - sphereBound.center = {0,0,0}; - sphereBound.radius = 10.0f; - computeInfo.bounds.sphereCount = 1; - computeInfo.bounds.spheres = &sphereBound; - xrComputeNewSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &computeInfo); - return {}; - } - - if (state != XR_SCENE_COMPUTE_STATE_COMPLETED_MSFT) return {}; - - // 2. Create the scene - XrSceneCreateInfoMSFT createInfo{XR_TYPE_SCENE_CREATE_INFO_MSFT}; - XrSceneMSFT scene; - if (xrCreateSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &createInfo, &scene) != XR_SUCCESS) return {}; - - // 3. Get mesh components - XrSceneComponentsGetInfoMSFT getInfo{XR_TYPE_SCENE_COMPONENTS_GET_INFO_MSFT}; - getInfo.componentType = XR_SCENE_COMPONENT_TYPE_MESH_MSFT; - - XrSceneComponentsMSFT components{XR_TYPE_SCENE_COMPONENTS_MSFT}; - xrGetSceneComponentsMSFT_ptr(scene, &getInfo, &components); - - std::vector componentBuffer(components.componentCountOutput); - components.componentBuffer = componentBuffer.data(); - components.componentCapacityInput = components.componentCountOutput; - xrGetSceneComponentsMSFT_ptr(scene, &getInfo, &components); - - // 4. Get locations for transforms - XrSceneComponentLocationsGetInfoMSFT locGetInfo{XR_TYPE_SCENE_COMPONENT_LOCATIONS_GET_INFO_MSFT}; - locGetInfo.baseSpace = (XrSpace)appSpace; - locGetInfo.time = frameState.predictedDisplayTime; - - std::vector locationBuffer(components.componentCountOutput); - XrSceneComponentLocationsMSFT locations{XR_TYPE_SCENE_COMPONENT_LOCATIONS_MSFT}; - locations.componentCount = components.componentCountOutput; - locations.componentLocations = locationBuffer.data(); - xrGetSceneComponentLocationsMSFT_ptr(scene, &locGetInfo, &locations); - - // 5. Get mesh data for each component - for (uint32_t i = 0; i < components.componentCountOutput; ++i) { - XrSceneMeshBuffersGetInfoMSFT meshGetInfo{XR_TYPE_SCENE_MESH_BUFFERS_GET_INFO_MSFT}; - meshGetInfo.meshComponentId = componentBuffer[i].id; - - XrSceneMeshBuffersMSFT meshBuffers{XR_TYPE_SCENE_MESH_BUFFERS_MSFT}; - xrGetSceneMeshBuffersMSFT_ptr(scene, &meshGetInfo, &meshBuffers); - - XrSpatialMesh mesh; - mesh.meshGuid = componentBuffer[i].id; - mesh.vertices.resize(meshBuffers.vertexCountOutput); - mesh.indices.resize(meshBuffers.indexCountOutput); - - XrSceneMeshVertexBufferMSFT vBuffer{XR_TYPE_SCENE_MESH_VERTEX_BUFFER_MSFT}; - vBuffer.vertexCapacityInput = (uint32_t)mesh.vertices.size(); - vBuffer.vertices = (XrVector3f*)mesh.vertices.data(); - - XrSceneMeshIndexBufferMSFT iBuffer{XR_TYPE_SCENE_MESH_INDEX_BUFFER_MSFT}; - iBuffer.indexCapacityInput = (uint32_t)mesh.indices.size(); - iBuffer.indices = mesh.indices.data(); - - meshBuffers.vertexBuffer = &vBuffer; - meshBuffers.indexBuffer = &iBuffer; - - xrGetSceneMeshBuffersMSFT_ptr(scene, &meshGetInfo, &meshBuffers); - - if (locationBuffer[i].flags & (XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT)) { - mesh.transform = xrPoseToMatrix(locationBuffer[i].pose); - } else { - mesh.transform = glm::mat4(1.0f); - } - - meshes.push_back(std::move(mesh)); - } - - // 6. Cleanup scene handle - xrDestroySceneMSFT_ptr(scene); - - // Start a new computation for the next call - XrSceneComputeInfoMSFT computeInfo{XR_TYPE_SCENE_COMPUTE_INFO_MSFT}; - computeInfo.consistency = XR_SCENE_COMPUTE_CONSISTENCY_SNAPSHOT_MSFT; - XrSceneSphereBoundMSFT sphereBound; - sphereBound.center = {0,0,0}; - sphereBound.radius = 10.0f; - computeInfo.bounds.sphereCount = 1; - computeInfo.bounds.spheres = &sphereBound; - xrComputeNewSceneMSFT_ptr((XrSceneObserverMSFT)sceneObserver, &computeInfo); - - return meshes; + return {}; } glm::vec2 XrContext::getGazeNDC() const { - if (!gazeSpace || views.empty()) return glm::vec2(0.5f, 0.5f); - - auto location = gazeSpace.locateSpace(appSpace, frameState.predictedDisplayTime); - if (!(location.locationFlags & xr::SpaceLocationFlagBits::OrientationValid)) { - return glm::vec2(0.5f, 0.5f); - } - - // Project gaze vector onto the near plane - // Gaze is along the -Z axis of the gaze space - glm::mat4 gazeMat = xrPoseToMatrix((XrPosef)location.pose); + if (gazeSpace == XR_NULL_HANDLE || views.empty()) return glm::vec2(0.5f, 0.5f); + XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION}; + xrLocateSpace(gazeSpace, appSpace, frameState.predictedDisplayTime, &loc); + if (!(loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) return glm::vec2(0.5f, 0.5f); + glm::mat4 gazeMat = xrPoseToMatrix(loc.pose); glm::vec3 gazeOrigin = glm::vec3(gazeMat[3]); - glm::vec3 gazeDir = -glm::vec3(gazeMat[2]); // Forward is -Z - - // Use the first eye's view/projection for NDC calculation + glm::vec3 gazeDir = -glm::vec3(gazeMat[2]); glm::mat4 viewProj = getProjectionMatrix(0) * getViewMatrix(0); glm::vec4 projected = viewProj * glm::vec4(gazeOrigin + gazeDir, 1.0f); - if (projected.w == 0.0f) return glm::vec2(0.5f, 0.5f); - glm::vec3 ndc = glm::vec3(projected) / projected.w; return glm::vec2(ndc.x * 0.5f + 0.5f, ndc.y * 0.5f + 0.5f); } diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h index 48177e6db..4d744ab5f 100644 --- a/attachments/openxr_engine/xr_context.h +++ b/attachments/openxr_engine/xr_context.h @@ -2,7 +2,9 @@ #include #include -#include +#define XR_USE_PLATFORM_XLIB +#define XR_USE_GRAPHICS_API_VULKAN +#include #include #include #include @@ -13,7 +15,7 @@ // Helper structure for spatial meshes (Chapter 16) struct XrSpatialMesh { - XrGuidMSFT meshGuid; + XrUuidMSFT meshGuid; std::vector vertices; std::vector indices; glm::mat4 transform; @@ -52,7 +54,7 @@ class XrContext { // Frame Lifecycle (Chapter 5) XrFrameState waitFrame(); void beginFrame(); - void endFrame(const std::array, 2>& eyeViews); + void endFrame(const std::array, 2>& eyeViews); // View & Projection (Chapter 4 & 11) void locateViews(XrTime predictedTime); @@ -80,13 +82,23 @@ class XrContext { static bool checkRuntimeAvailable(); private: - xr::Instance instance; + PFN_xrGetVulkanInstanceExtensionsKHR pfnGetVulkanInstanceExtensionsKHR = nullptr; + PFN_xrGetVulkanDeviceExtensionsKHR pfnGetVulkanDeviceExtensionsKHR = nullptr; + PFN_xrGetVulkanGraphicsRequirementsKHR pfnGetVulkanGraphicsRequirementsKHR = nullptr; + PFN_xrGetVulkanGraphicsRequirements2KHR pfnGetVulkanGraphicsRequirements2KHR = nullptr; + PFN_xrGetVulkanGraphicsDeviceKHR pfnGetVulkanGraphicsDeviceKHR = nullptr; + PFN_xrGetVulkanGraphicsDevice2KHR pfnGetVulkanGraphicsDevice2KHR = nullptr; + + XrInstance instance; vk::Instance vkInstance; - xr::SystemId systemId; - xr::Session session; - xr::Space appSpace; + XrSystemId systemId; + XrSession session; + XrSpace appSpace; XrReferenceSpaceType referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + uint8_t requiredLuid[VK_LUID_SIZE] = {0}; + bool luidValid = false; + #if defined(PLATFORM_ANDROID) struct android_app* androidApp = nullptr; #endif @@ -95,7 +107,7 @@ class XrContext { vk::Extent2D extent; struct SwapchainData { - xr::Swapchain handle; + XrSwapchain handle; std::vector images; }; std::vector swapchains; @@ -104,16 +116,16 @@ class XrContext { std::vector views; // Action system members - xr::ActionSet actionSet; - std::map actions; - std::map actionTypes; - std::map actionSpaces; + XrActionSet actionSet; + std::map actions; + std::map actionTypes; + std::map actionSpaces; // Gaze interaction member - xr::Space gazeSpace; + XrSpace gazeSpace; // Scene understanding member - xr::SceneObserverMSFT sceneObserver; + // XrSceneObserverMSFT sceneObserver; std::vector enabledExtensions; }; From 392c995d58ea2acc3a59434f90cf076f21d66c67 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 17 Mar 2026 01:14:14 -0700 Subject: [PATCH 05/23] Fix xref navigation paths across OpenXR Vulkan Spatial Computing guide and update XrGuidMSFT to XrUuidMSFT --- attachments/openxr_engine/renderer_core.cpp | 2 +- .../Tooling/index.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_system_integration.adoc | 2 +- .../03_hardware_alignment_luid.adoc | 10 +++-- .../04_vulkan_1_4_feature_requirements.adoc | 2 +- .../05_incorporating_into_the_engine.adoc | 40 +++++++++---------- .../01_introduction.adoc | 2 +- .../02_external_image_negotiation.adoc | 2 +- .../03_raii_resource_integration.adoc | 2 +- .../04_memory_ownership_lifecycle.adoc | 2 +- .../05_incorporating_into_the_engine.adoc | 29 +++++++------- .../04_Dynamic_Rendering/01_introduction.adoc | 2 +- .../02_rendering_to_spatial_swapchains.adoc | 2 +- .../03_stereo_viewport_scissor.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 19 +++++---- .../01_introduction.adoc | 2 +- .../02_xr_lifecycle.adoc | 2 +- .../03_display_time_prediction.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../06_Late_Latching/01_introduction.adoc | 2 +- .../02_last_second_update.adoc | 2 +- .../06_Late_Latching/03_implementation.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_openxr_action_system.adoc | 2 +- .../03_space_manifolds.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_native_multiview.adoc | 2 +- .../03_slang_architecture.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_primary_stereo_with_insets.adoc | 2 +- .../03_multi_layer_composition.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_fragment_density_control.adoc | 2 +- .../03_gaze_driven_logic.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../11_Canted_Displays/01_introduction.adoc | 2 +- .../02_non_parallel_projections.adoc | 2 +- .../03_viewport_swizzling.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../12_CAVE_Architecture/01_introduction.adoc | 2 +- .../02_projector_based_spatial_tech.adoc | 2 +- .../03_hardware_sync.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../13_Warp_and_Blend/01_introduction.adoc | 2 +- .../02_geometric_correction.adoc | 2 +- .../03_post_process_warping.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../14_LightField_Theory/01_introduction.adoc | 2 +- .../02_4d_lightfield_representation.adoc | 2 +- .../03_high_density_view_arrays.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_synthesis_shaders.adoc | 2 +- .../03_ray_traced_synthesis.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_environmental_ingestion.adoc | 2 +- .../03_zero_copy_hand_off.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 6 +-- .../01_introduction.adoc | 2 +- .../02_on_gpu_inference.adoc | 2 +- .../03_refining_spatial_data.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_ml_driven_segmentation.adoc | 2 +- .../03_per_pixel_masking.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_desktop_high_end.adoc | 2 +- .../03_mobile_mastery.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_spatial_debugging.adoc | 2 +- .../03_automated_qa.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../conclusion.adoc | 2 +- .../introduction.adoc | 6 +-- 82 files changed, 131 insertions(+), 131 deletions(-) diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index 51c24d344..16320dc50 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -718,7 +718,7 @@ bool Renderer::pickPhysicalDevice() { vk::PhysicalDeviceIDProperties idProps; vk::PhysicalDeviceProperties2 props2; props2.pNext = &idProps; - props2 = _device.getProperties2(); + _device.getProperties2(&props2); const uint8_t* requiredLuid = xrContext.getRequiredLUID(); if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) { diff --git a/en/Building_a_Simple_Engine/Tooling/index.adoc b/en/Building_a_Simple_Engine/Tooling/index.adoc index 783752636..cd18ccdfc 100644 --- a/en/Building_a_Simple_Engine/Tooling/index.adoc +++ b/en/Building_a_Simple_Engine/Tooling/index.adoc @@ -12,4 +12,4 @@ This chapter covers essential tooling and techniques for developing, debugging, * xref:Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc[Packaging and Distribution] * xref:Building_a_Simple_Engine/Tooling/07_conclusion.adoc[Conclusion] -xref:Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | xref:../index.adoc[Back to Building a Simple Engine] +xref:Building_a_Simple_Engine/Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | xref:Building_a_Simple_Engine/introduction.adoc[Back to Building a Simple Engine] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc index b14f11518..80a6add41 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc @@ -23,4 +23,4 @@ If you don't perform this handshake correctly, you might find that you can't ini By the end of this chapter, you will have modified your engine's initialization code to be fully spatial-aware, laying the groundwork for the predictive frame loop and runtime-owned swapchains that follow. -xref:../introduction.adoc[Previous] | xref:02_system_integration.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc index 9375dd1b8..725b6ec38 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc @@ -78,4 +78,4 @@ By following the `XR_KHR_vulkan_enable2` protocol, we guarantee that our frames In the next section, we will look at **Hardware Alignment**, where we ensure that the `VkPhysicalDevice` we select is the exact same one the headset is physically connected to. -xref:01_introduction.adoc[Previous] | xref:03_hardware_alignment_luid.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc index 153dca76f..4b6cc6d4a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc @@ -40,12 +40,14 @@ To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDPr ---- // Iterating through physical devices to find a match for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { - auto props2 = physicalDevice.getProperties2(); - auto idProps = props2.get(); + vk::PhysicalDeviceIDProperties idProps; + vk::PhysicalDeviceProperties2 props2; + props2.pNext = &idProps; + physicalDevice.getProperties2(&props2); if (idProps.deviceLUIDValid) { // Compare the 8-byte LUID against the target from OpenXR - if (memcmp(idProps.deviceLUID.data(), targetLUID, 8) == 0) { + if (std::memcmp(idProps.deviceLUID, targetLUID, VK_LUID_SIZE) == 0) { return physicalDevice; } } @@ -60,4 +62,4 @@ Vulkan 1.4 makes this easier by standardizing external memory handles, but these In the final part of our handshake, we will ensure that our selected device is configured with the mandatory **Vulkan 1.4 features** required for a modern spatial pipeline. -xref:02_system_integration.adoc[Previous] | xref:04_vulkan_1_4_feature_requirements.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc index 2b7f99d65..137de6b96 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc @@ -69,4 +69,4 @@ We have now successfully: With the handshake complete, we are ready to tackle the most significant architectural change in an XR engine: moving from engine-owned swapchains to **Runtime-Owned Swapchains**. -xref:03_hardware_alignment_luid.adoc[Previous] | xref:05_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc index 7fce8783f..66bbb7e72 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc @@ -49,17 +49,17 @@ public: // Swapchain Management (Chapter 3) void createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent); - std::vector enumerateSwapchainImages(uint32_t eye); + std::vector enumerateSwapchainImages(); vk::Extent2D getSwapchainExtent(); vk::Format getSwapchainFormat(); void waitSwapchainImage(); - uint32_t acquireSwapchainImage(uint32_t eye); - void releaseSwapchainImage(uint32_t eye); + uint32_t acquireSwapchainImage(); + void releaseSwapchainImage(); // Frame Lifecycle (Chapter 5) XrFrameState waitFrame(); void beginFrame(); - void endFrame(const std::array, 2>& views); + void endFrame(const std::array, 2>& eyeViews); // View & Projection (Chapter 4 & 11) void locateViews(XrTime predictedTime); @@ -131,16 +131,16 @@ In `Renderer::pickPhysicalDevice`, we must ensure we select the GPU that OpenXR bool Renderer::pickPhysicalDevice() { // ... enumerate devices ... - vk::PhysicalDeviceIDProperties idProps; - vk::PhysicalDeviceProperties2 props2; - props2.pNext = &idProps; - for (auto& _device : devices) { - _device.getProperties2(&props2); - if (xrMode) { // Match the LUID provided by OpenXR - if (std::memcmp(idProps.deviceLUID, xrContext.getRequiredLUID(), VK_LUID_SIZE) != 0) { + vk::PhysicalDeviceIDProperties idProps; + vk::PhysicalDeviceProperties2 props2; + props2.pNext = &idProps; + _device.getProperties2(&props2); + + const uint8_t* requiredLuid = xrContext.getRequiredLUID(); + if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) { continue; // Not the right GPU for XR! } } @@ -149,26 +149,26 @@ bool Renderer::pickPhysicalDevice() { } ---- -=== 3. Enabling Vulkan 1.4 Features +=== 3. Enabling Vulkan 1.3/1.4 Features -In `Renderer::createLogicalDevice`, we must explicitly enable the 1.4 features required for spatial computing: +In `Renderer::createLogicalDevice`, we must explicitly enable the Vulkan 1.3 features required for spatial computing, particularly **Dynamic Rendering** and **Synchronization 2**: [source,cpp] ---- // renderer_core.cpp bool Renderer::createLogicalDevice(bool enableValidationLayers) { // ... + // Enable Vulkan 1.3 features vk::PhysicalDeviceVulkan13Features vulkan13Features; vulkan13Features.dynamicRendering = vk::True; vulkan13Features.synchronization2 = vk::True; - // NEW: OpenXR requires Timeline Semaphores (often a 1.2 feature, standard in 1.4) - vk::PhysicalDeviceTimelineSemaphoreFeatures timelineFeatures; - timelineFeatures.timelineSemaphore = vk::True; - - vulkan13Features.pNext = &timelineFeatures; + // Enable Multiview (if in XR mode) + vk::PhysicalDeviceMultiviewFeatures multiviewFeatures; + multiviewFeatures.multiview = xrMode ? vk::True : vk::False; + multiviewFeatures.pNext = &vulkan13Features; - // ... create vk::raii::Device ... + // ... create vk::raii::Device with multiviewFeatures in pNext ... } ---- @@ -176,4 +176,4 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { By referencing the specific locations in `renderer_core.cpp`, we can see that OpenXR isn't just an "add-on"—it's a **collaborator** in the Vulkan lifecycle. Without the LUID matching in `pickPhysicalDevice`, our engine might initialize on a discrete GPU while the VR headset is tethered to an integrated one, leading to a complete failure to display. Similarly, the extension negotiation ensures that the XR compositor and our engine speak the same "dialect" of Vulkan. -xref:04_vulkan_1_4_feature_requirements.adoc[Previous] | xref:../03_Runtime_Owned_Swapchains/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc index 5b790b8f9..bc1cec9db 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc @@ -23,4 +23,4 @@ This rhythm ensures that we never render into an image that is currently being d By the end of this chapter, your engine will be able to render its spatial views directly into the headset's compositor buffers, using our existing RAII wrappers while respecting the runtime's ownership. -xref:../02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Previous] | xref:02_external_image_negotiation.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc index 741a8dc51..3a4082dc5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc @@ -71,4 +71,4 @@ By negotiating the images this way, we ensure a **Zero-Copy** hand-off. When we This efficiency is the difference between a high-performance 90Hz experience and a laggy, uncomfortable one. In the next section, we'll see how to wrap these raw Vulkan handles into the engine's `vk::raii` abstractions. -xref:01_introduction.adoc[Previous] | xref:03_raii_resource_integration.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc index 8b0cf4c83..4201d67dc 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc @@ -72,4 +72,4 @@ This hybrid approach—using raw handles for external resources and RAII for int In the next section, we will look at the **Memory Ownership Lifecycle**, where we master the delicate dance of waiting, acquiring, and releasing these images during our frame loop. -xref:02_external_image_negotiation.adoc[Previous] | xref:04_memory_ownership_lifecycle.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc index fcc79fc71..3f254f32f 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc @@ -84,4 +84,4 @@ We have now seen how to: With our resources properly managed, we can now move on to the actual rendering process. In the next chapter, we will see how to use **Dynamic Rendering** to render our spatial views directly into these images with maximum efficiency. -xref:03_raii_resource_integration.adoc[Previous] | xref:05_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc index b5d16f5b4..0c35f706e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc @@ -12,16 +12,18 @@ In `renderer_rendering.cpp`, we need to modify `Renderer::createSwapChain` to by // renderer_rendering.cpp bool Renderer::createSwapChain() { if (xrMode) { - // NEW: Enumerate images provided by the OpenXR runtime for each eye - for (uint32_t eye = 0; eye < 2; eye++) { - eyeSwapchainImages[eye] = xrContext.enumerateSwapchainImages(eye); - } + // NEW: Negotiate with OpenXR instead of the windowing system + vk::Extent2D xrExtent = xrContext.getRecommendedExtent(); + xrContext.createSwapchains(*device, vk::Format::eB8G8R8A8Srgb, xrExtent); + + // Use a single swapchain with 2 layers for multiview + eyeSwapchainImages[0] = xrContext.enumerateSwapchainImages(); + eyeSwapchainImages[1].clear(); // Not used in multiview mode - // Store extent provided by OpenXR (usually fixed per-headset) swapChainExtent = xrContext.getSwapchainExtent(); swapChainImageFormat = xrContext.getSwapchainFormat(); - return createImageViews(); // Re-use existing view creation logic + return createImageViews(); } // ... original desktop vk::raii::SwapchainKHR logic ... @@ -40,13 +42,11 @@ void Renderer::Render(...) { uint32_t imageIndex; if (xrMode) { - // 1. WAIT: Ensure the runtime is ready for us to start working - xrContext.waitSwapchainImage(); + // 1. ACQUIRE: Get the index of the next available XR image from the runtime + imageIndex = xrContext.acquireSwapchainImage(); - // 2. ACQUIRE: Get the index of the next available XR image for the current eye - // Note: For simplicity, we'll expand this to a per-eye loop in Chapter 4 - uint32_t eye = 0; - imageIndex = xrContext.acquireSwapchainImage(eye); + // 2. WAIT: Block until the image is safe to be written to + xrContext.waitSwapchainImage(); } else { // Standard desktop acquisition auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); @@ -57,8 +57,7 @@ void Renderer::Render(...) { if (xrMode) { // 3. RELEASE: Hand the image back to the compositor for display - uint32_t eye = 0; - xrContext.releaseSwapchainImage(eye); + xrContext.releaseSwapchainImage(); } else { // Standard desktop present presentQueue.presentKHR(presentInfo); @@ -70,4 +69,4 @@ void Renderer::Render(...) { By injecting these calls into `createSwapChain` and `Render`, we maintain the engine's high-level abstractions (like `vk::raii::Image`) while relinquishing control of the **lifecycle** to the XR runtime. The `xrContext` serves as a bridge, ensuring that the engine never tries to draw into an image that the compositor is currently reading to project onto the headset's lenses. -xref:04_memory_ownership_lifecycle.adoc[Previous] | xref:../04_Dynamic_Rendering/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc index 56cb8bceb..58a9c5993 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc @@ -13,4 +13,4 @@ For spatial computing, the dynamic rendering system we've already built offers t In this chapter, we will apply our engine's rendering logic to the unique requirements of spatial displays. We'll start by looking at how to bind our XR swapchain images directly to our rendering session and then move on to managing the complex viewports required by modern headsets. -xref:../03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Previous] | xref:02_rendering_to_spatial_swapchains.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc index 5ce91b145..ac51a811b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc @@ -59,4 +59,4 @@ By using dynamic rendering, we've successfully decoupled our engine's internal r This **Separation of Concerns** is what allows our engine to scale seamlessly. Whether we are rendering to a 4K desktop monitor or a dual-eye VR headset, the core command recording logic remains the same. The only thing that changes is *where* the image view points and *how* the projection matrices are calculated. -xref:01_introduction.adoc[Previous] | xref:03_stereo_viewport_scissor.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc index 0ec473568..3118d1bad 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc @@ -54,4 +54,4 @@ By treating viewports and scissors as dynamic state, we avoid the heavy "Pipelin With our rendering pipeline now fully adapted to the unique requirements of spatial views, we are ready to tackle the timing and pacing logic that makes XR truly immersive. In the next chapter, we will build our **Predictive Frame Loop**. -xref:02_rendering_to_spatial_swapchains.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc index e0f312725..de163dc05 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc @@ -12,16 +12,14 @@ In `renderer_rendering.cpp`, our `Render` function already uses `vk::RenderingIn // renderer_rendering.cpp void Renderer::Render(...) { // ... Wait for runtime to be ready ... + uint32_t imageIndex = xrContext.acquireSwapchainImage(); xrContext.waitSwapchainImage(); for (uint32_t eye = 0; eye < 2; ++eye) { - // 1. Acquire the next image for this eye - uint32_t imageIndex = xrContext.acquireSwapchainImage(eye); - - // 2. Point rendering at the acquired XR image view - // xrImageViews is our per-eye vector of swapchain image views + // 1. Point rendering at the acquired XR image view + // Our engine uses a single multiview swapchain at index [0] vk::RenderingAttachmentInfo colorAttachment{ - .imageView = *eyeSwapchainImageViews[eye][imageIndex], + .imageView = *eyeSwapchainImageViews[0][imageIndex], .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, @@ -31,6 +29,7 @@ void Renderer::Render(...) { vk::RenderingInfo passInfo{ .renderArea = vk::Rect2D({0, 0}, swapChainExtent), .layerCount = 1, + .viewMask = (1u << eye), // Render specifically to this eye's layer .colorAttachmentCount = 1, .pColorAttachments = &colorAttachment }; @@ -41,10 +40,10 @@ void Renderer::Render(...) { RenderEye(commandBuffer, eye); commandBuffer.endRendering(); - - // 3. Hand the image back to OpenXR - xrContext.releaseSwapchainImage(eye); } + + // 2. Hand the image back to OpenXR + xrContext.releaseSwapchainImage(); } ---- @@ -107,4 +106,4 @@ void Renderer::updateUniformBuffer(uint32_t currentFrame, uint32_t eye, const Xr By binding the XR image directly into `vk::RenderingInfo` and updating viewports/scissors per-eye, we eliminate the need for an intermediate "Blit" pass. This ensures that every GPU cycle is spent on spatial content, not memory copies. At this stage, our shaders remain unchanged because we are essentially just "drawing the world twice" from different perspectives. In later chapters, we'll see how **Multiview** can collapse these two passes into one. -xref:03_stereo_viewport_scissor.adoc[Previous] | xref:../05_Predictive_Frame_Loop/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc index d1cd1ba8c..45296508d 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc @@ -23,4 +23,4 @@ In this chapter, we will transition our engine's main loop from its legacy `glfw By the end of this chapter, your engine will no longer be "reacting" to user movement; it will be "anticipating" it, providing the smooth, low-latency experience that defines high-quality spatial computing. -xref:../04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Previous] | xref:02_xr_lifecycle.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc index 897ead42e..37201efdd 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc @@ -68,4 +68,4 @@ Our engine's main loop now follows this precise rhythm: By following this rhythm, we ensure our engine is perfectly synchronized with the headset's hardware, providing the smooth and stable experience required for spatial computing. -xref:01_introduction.adoc[Previous] | xref:03_display_time_prediction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc index 90ecd4126..04b1feb47 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc @@ -53,4 +53,4 @@ By using only the `predictedDisplayTime` provided by the XR runtime, we ensure t Now that our frame loop is correctly paced and our simulation is aligned with the future, we have established the foundation for a high-quality spatial experience. However, there is still a small window of latency between the end of our command buffer recording and the absolute final moment before submission. In the next chapter, we will explore **Late Latching** and how **Timeline Semaphores** can help us shave off those final microseconds of "swim." -xref:02_xr_lifecycle.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc index 7f6e1575d..4b18c9cd0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc @@ -109,4 +109,4 @@ void Engine::Update(XrTime predictedTime) { By moving `xrWaitFrame` into our main loop, we transform the engine from an "as-fast-as-possible" renderer into a "just-in-time" spatial engine. This eliminates the judder caused by simulation/display mismatch and ensures that when the user moves their head, the virtual world moves in perfect, predicted synchrony. -xref:03_display_time_prediction.adoc[Previous] | xref:../06_Late_Latching/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc index 7987d516f..23d91e955 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc @@ -15,4 +15,4 @@ In this chapter, we will look at how to: 2. Update our shader data at the "last microsecond" before submission. 3. Integrate these late-breaking updates into the engine's command buffer flow. -xref:../05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Previous] | xref:02_last_second_update.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc index 4b1e58dc7..25751597a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc @@ -49,4 +49,4 @@ queue.submit2(submitInfo); By using the wait-before-submit pattern, we reduce that gap from ~10-15ms down to ~1-2ms. The virtual world feels "anchored" to the physical space because the view matrices being used by the GPU were updated only microseconds before the first vertex was processed. -xref:01_introduction.adoc[Previous] | xref:03_implementation.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc index 7571b9882..bbd418549 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc @@ -60,4 +60,4 @@ By focusing purely on the view and projection matrices, we keep the overhead min With late latching implemented, our virtual environment is now as anchored as possible. In the next chapter, we will shift our focus to **Action Spaces**—how we turn user input (button clicks and hand movements) into meaningful interactions within this spatially-aligned world. -xref:02_last_second_update.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc index 87a23b534..871ce25f8 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc @@ -57,4 +57,4 @@ bool Renderer::createUniformBuffers(Entity* entity) { By moving the `updateUniformBuffer` call to the very end of the `Render` function (just before submission), we ensure that the GPU receives the most accurate head-tracking data possible. This reduces the "perceived latency" (Motion-to-Photon) from the time we recorded the frame to the time it's actually scanned out to the headset's display. -xref:03_implementation.adoc[Previous] | xref:../07_Action_Spaces_Input/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc index 054d69fcd..403345294 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc @@ -14,4 +14,4 @@ In this chapter, we will explore: By the end of this section, we will have a robust system for handling both digital input (button clicks) and spatial input (hand positions) in a way that is clean, modular, and performant. -xref:../06_Late_Latching/04_incorporating_into_the_engine.adoc[Previous] | xref:02_openxr_action_system.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc index c17749d40..87e02de8e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc @@ -77,4 +77,4 @@ xrSuggestInteractionProfileBindings(instance, &profileBinding); This abstraction is especially critical for **Space Manifolds**, where a user's physical movement is converted into a 3D coordinate system for our renderer—the topic of our next section. -xref:01_introduction.adoc[Previous] | xref:03_space_manifolds.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc index 2d38f20f0..d2930dff7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc @@ -52,4 +52,4 @@ This transformation process effectively turns a physical controller into a **Spa In the next chapter, we will look at how to optimize the rendering of these multiple views using **Slang for Spatial Shaders**. -xref:02_openxr_action_system.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc index 6a2a87d6b..d2ca2b852 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc @@ -43,4 +43,4 @@ void Engine::Update(XrTime predictedTime) { By decoupling input from physical hardware (like a specific controller's trigger), we make our engine cross-platform out of the box. Whether the user is using an Oculus Touch controller or a Valve Index "knuckle," the engine only sees the "Grab" action and the associated spatial pose, allowing us to focus on the physics of the interaction rather than the nuances of different hardware drivers. -xref:03_space_manifolds.adoc[Previous] | xref:../08_Slang_Spatial_Shaders/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc index fbe350ba0..8a9b859a7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc @@ -20,4 +20,4 @@ We'll define some key concepts like **SIMD** (Single Instruction, Multiple Data) In the following sections, we'll dive deep into the implementation of **Native Multiview** and then see how to architect our **Slang** shaders to take full advantage of this hardware-level optimization. -xref:../07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Previous] | xref:02_native_multiview.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc index 6c21365ba..b8471e82a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc @@ -63,4 +63,4 @@ It's important to understand that while the vertex work is duplicated, the **Pri In the next section, we'll see how to architect our **Slang** shaders to manage this "N-View" logic efficiently. -xref:01_introduction.adoc[Previous] | xref:03_slang_architecture.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc index 92c1da36a..850538635 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc @@ -58,4 +58,4 @@ This significantly reduces the chances of errors. For example, if you add a thir In the next chapter, we'll see how to push this "N-View" mindset even further by looking at **Quad-Views** and **Foveated Rendering**. -xref:02_native_multiview.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc index 499d56f72..10c7d8ac1 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc @@ -73,4 +73,4 @@ VS_Output vertexMain(VS_Input input, uint viewID : SV_ViewID) { By enabling the `viewMask` and using `SV_ViewID`, we shift the burden of stereo rendering from the CPU to the GPU. Instead of the engine looping twice and submitting two sets of commands, it submits one set, and the hardware's **Instance Data Step** or **Geometry Replication** handles the expansion to two views. This is the most efficient way to achieve high-performance spatial rendering in Vulkan 1.4. -xref:03_slang_architecture.adoc[Previous] | xref:../09_Quad_Views_Foveated/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc index 351c30e77..33f0d00d7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc @@ -11,4 +11,4 @@ We'll define some key terms like **PPS** (Pixel per Degree) and see how to manag In the following sections, we'll dive into the details of architecting these primary and inset view layers, and we'll see how to efficiently submit them to the OpenXR compositor for the final image. -xref:../08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Previous] | xref:02_primary_stereo_with_insets.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc index 70184717d..820ea84a4 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc @@ -57,4 +57,4 @@ VertexOutput main(uint viewID : SV_ViewID, float3 pos : POSITION) { Crucially, the **Inset View** isn't just a zoomed-in version of the base view. It's a high-density image that captures more detail from the 3D scene. We are effectively focusing the GPU's rasterization power on a smaller subset of the world. -xref:01_introduction.adoc[Previous] | xref:03_multi_layer_composition.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc index 0d62156c4..57baf5fe8 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc @@ -46,4 +46,4 @@ This multi-layer approach is incredibly flexible. Each layer can even have its o In the next chapter, we'll see how to push our performance even further by looking at **Variable Rate Shading** (VRS). -xref:02_primary_stereo_with_insets.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc index 36be53522..64f26606e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc @@ -64,4 +64,4 @@ bool Renderer::createGraphicsPipeline() { By moving to a multi-layer composition, we enable "Foveal Rendering" where the engine renders the center of the user's view at a much higher resolution than the periphery. Because our engine's `Render` loop is already dynamic, it can seamlessly switch between these views and layers, ensuring that the user always experiences the highest fidelity where they are actually looking. -xref:03_multi_layer_composition.adoc[Previous] | xref:../10_Variable_Rate_Shading/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc index 290df0a66..a6bf34e87 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc @@ -13,4 +13,4 @@ We will focus on two primary strategies: By the end of this chapter, you will understand how to integrate the **VK_KHR_fragment_shading_rate** extension (now part of the Vulkan 1.4 core) into your spatial pipeline and how to manage shading rate maps that update in real-time. -xref:../09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Previous] | xref:02_fragment_density_control.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc index 49785b65b..655784e75 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc @@ -41,4 +41,4 @@ Because XR lenses are naturally bluer and more distorted at the edges, we can pr When combined with **Multiview**, a single shading rate map can be applied to both eyes simultaneously, saving massive amounts of GPU time without affecting the user's immersion. -xref:01_introduction.adoc[Previous] | xref:03_gaze_driven_logic.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc index 0eff80b4c..1f37abb5e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc @@ -40,4 +40,4 @@ When implemented correctly, DFR allows you to push the boundaries of visual fide In the next chapter, we will shift from software-driven optimization to hardware-driven geometry by looking at **Canted Displays and Asymmetric Frustums**. -xref:02_fragment_density_control.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc index dbdb0f4b6..ebec77e0c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc @@ -79,4 +79,4 @@ void Renderer::updateShadingRateImage(glm::vec2 gazeNDC) { By integrating eye-tracking data directly into our Vulkan 1.4 rendering pass, we enable **Dynamic Foveated Rendering (DFR)**. This allows the engine to save up to 60% of fragment processing costs in complex PBR scenes by only rendering the tiny area the user is actually looking at in full detail. Because our engine uses dynamic rendering, adding the `vrsAttachment` to our `passInfo` is a simple `pNext` extension away. -xref:03_gaze_driven_logic.adoc[Previous] | xref:../11_Canted_Displays/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc index 833836a14..3185c4923 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc @@ -12,4 +12,4 @@ In this chapter, we will explore how to handle these non-parallel architectures By mastering these concepts, your engine will be compatible with the entire spectrum of spatial hardware, from mobile VR headsets with parallel screens to ultra-wide FOV enthusiast hardware. -xref:../10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Previous] | xref:02_non_parallel_projections.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc index 32f78aa01..040bbbd30 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc @@ -49,4 +49,4 @@ On headsets like the Valve Index or Pimax, the display panels themselves are rot When you use **Multiview**, you must push an array of projection matrices and an array of view matrices to your Slang shaders. If you try to use a single "stereo" matrix with a fixed offset, the world will appear warped and uncomfortable because the GPU is drawing a rectilinear world for a non-rectilinear display. -xref:01_introduction.adoc[Previous] | xref:03_viewport_swizzling.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc index 42de694c6..fa0f1bd89 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc @@ -37,4 +37,4 @@ In a modern spatial engine, you should treat swizzling as a "Hardware Driver" fe In the next chapter, we will look at how to scale these spatial techniques to even larger environments using **CAVE Architecture and Multi-GPU Synchronization**. -xref:02_non_parallel_projections.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc index ed6be658d..b2b647b0c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc @@ -59,4 +59,4 @@ void Engine::Update(XrTime predictedTime) { By supporting canted views and asymmetric frustums, our engine becomes compatible with high-end "enthusiast" headsets. Proper projection matrix calculation is the difference between a world that looks "right" and one that feels slightly distorted or "warped" as you move your head. Because we've already integrated the OpenXR handshake, our `xrContext` can provide the exact FOV tangents and poses required for these calculations. -xref:03_viewport_swizzling.adoc[Previous] | xref:../12_CAVE_Architecture/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc index f760d042f..c9956a5f3 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc @@ -25,4 +25,4 @@ Without these hardware-level guarantees, the "immersion" of a CAVE is quickly br By the end of this chapter, you will understand how to scale your spatial engine from a single-user headset to a multi-node, room-scale immersive environment. -xref:../11_Canted_Displays/04_incorporating_into_the_engine.adoc[Previous] | xref:02_projector_based_spatial_tech.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc index 25784b2a3..c9933c335 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc @@ -44,4 +44,4 @@ In a cluster, every computer must agree on the state of the world. If a particle To the user, this cluster behaves as a single, massive GPU. However, this only works if every node renders and displays the exact same frame at the exact same time. -xref:01_introduction.adoc[Previous] | xref:03_hardware_sync.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc index efdf8ff72..6b550e0f1 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc @@ -49,4 +49,4 @@ if (barrierFeatures.presentBarrier) { By combining Genlock (signal timing) with Swap Barriers (frame timing), we achieve the "Gold Standard" of spatial synchronization: **Frame-Perfect Cluster Rendering**. The entire room updates as if it were a single piece of digital paper. -xref:02_projector_based_spatial_tech.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc index 0c395ad32..c5822077a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc @@ -47,4 +47,4 @@ void Renderer::Render(...) { Hardware synchronization is the only way to ensure that the corners of a CAVE room look seamless. Without the `VK_NV_present_barrier`, the "seam" between projectors will always be visible as a line of tearing or stutter. By integrating this into our `createSwapChain` and `Render` logic, we transform the engine into a professional-grade simulation platform capable of driving multi-projector environments. -xref:03_hardware_sync.adoc[Previous] | xref:../13_Warp_and_Blend/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc index 8cc388802..d15acd191 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc @@ -25,4 +25,4 @@ Warping is only as good as the data driving it. Most professional installations By the end of this chapter, you will be able to take a standard rectilinear render and "warp" it into any physical shape required by the installation, ensuring that the virtual world remains perfectly undistorted to the user's eyes. -xref:../12_CAVE_Architecture/04_incorporating_into_the_engine.adoc[Previous] | xref:02_geometric_correction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc index 82eae6e06..e2833abc9 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc @@ -58,4 +58,4 @@ In a multi-projector setup, the edges of the images overlap. Because warping involves sampling a texture at non-integer coordinates, the quality of your filtering is paramount. While `SamplerState` with linear filtering is fast, it can lead to "blurring" in areas of high warp. High-end spatial engines often implement custom **Bicubic** or **Lanczos** filters in the compute shader to preserve sharpness during the coordinate transformation. -xref:01_introduction.adoc[Previous] | xref:03_post_process_warping.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc index 055f547be..2b8b06cd9 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc @@ -53,4 +53,4 @@ Because warping "squeezes" the image, some areas of your render will be scaled d In the next chapter, we will move from pixels to 4D functions by exploring **LightField Theory**. -xref:02_geometric_correction.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc index 7b48237e1..716454346 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc @@ -71,4 +71,4 @@ float3 calculateEdgeBlend(uint2 pos) { Proper warping and blending are what make a CAVE room feel like a unified environment. By handling distortion in the engine's compute-based post-processing chain, we can support a wide variety of hardware—from curved screens to custom headsets—without changing our core rasterization logic. Because our engine already has a modular `Renderer`, adding this compute pass is a natural extension of our existing frame loop. -xref:03_post_process_warping.adoc[Previous] | xref:../14_LightField_Theory/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc index 895db3f97..6c8a6fe50 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc @@ -9,4 +9,4 @@ In this chapter, we are going to explore how we can simplify this monster into a This 4D LightField is the "Holy Grail" for high-end spatial displays. It allows us to render holographic images where your eyes can naturally focus at different depths (solving the Vergence-Accommodation Conflict, or **VAC**) and see different perspectives as you move your head, all without the need for expensive per-eye re-rendering. We will look at how to define these rays using the classic "Two-Plane Parametrization"—the **ST plane** (spatial position) and the **UV plane** (directional orientation)—and how to map this mathematical grid into a high-performance memory layout for the engine. -xref:../13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Previous] | xref:02_4d_lightfield_representation.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc index 06240d387..36e380ff7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc @@ -62,4 +62,4 @@ uint getLinearIndex(uint2 st, uint2 uv, uint2 stRes, uint2 uvRes) { By using this representation, the engine can treat a lightfield as a simple array of "light fragments," making it compatible with existing spatial pipelines while providing the foundation for complex plenoptic synthesis. -xref:01_introduction.adoc[Previous] | xref:03_high_density_view_arrays.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc index 56aac19c0..025931062 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc @@ -58,4 +58,4 @@ By combining data compression, raw pointer flexibility via BDA, and tiled memory In the next chapter, we will see how to turn these rays back into images using **Plenoptic Synthesis**. -xref:02_4d_lightfield_representation.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc index 16bd50665..51ed0f0ce 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc @@ -58,4 +58,4 @@ void Renderer::createLightFieldBuffer(size_t totalBytes) { By moving from traditional descriptor sets to BDA for lightfield data, we avoid the overhead of binding thousands of individual view textures. Our engine can now manage massive plenoptic datasets (often multi-gigabyte) as a single contiguous block of memory. This ensures maximum cache locality during the synthesis pass, which is critical for maintaining spatial immersion. -xref:03_high_density_view_arrays.adoc[Previous] | xref:../15_Plenoptic_Synthesis/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc index ecd9b8081..ae7b70581 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc @@ -9,4 +9,4 @@ In this chapter, we will explore the two primary paths for plenoptic synthesis: We'll look at how the engine coordinates these two paths using compute and fragment shaders and how to use Slang's productivity features to author complex synthesis logic without getting lost in the 4D math. -xref:../14_LightField_Theory/04_incorporating_into_the_engine.adoc[Previous] | xref:02_synthesis_shaders.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc index 9af23bfe1..2a3e955a2 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc @@ -47,4 +47,4 @@ Because we are doing 16 lookups per pixel, we must be extremely mindful of **SIM By using the **4D Tiling** we implemented in Chapter 14, we ensure that these 16 samples are geographically close in physical VRAM. This allows the GPU to satisfy all 16 requests with just one or two cache line fetches, keeping our plenoptic synthesis running at high frame rates. -xref:01_introduction.adoc[Previous] | xref:03_ray_traced_synthesis.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc index 6445b6528..bb83e57cd 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc @@ -46,4 +46,4 @@ Furthermore, ray tracing allows us to handle **Non-Uniform LightFields**. If our By combining the 4D theory of LightFields with the physical accuracy of Vulkan Ray Tracing, we create a spatial rendering pipeline that can handle the most advanced holographic displays on the market. -xref:02_synthesis_shaders.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc index 910310fa2..1a04f40cc 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc @@ -72,4 +72,4 @@ void raygenMain() { By providing dual paths in our `Renderer`, we allow the engine to scale from low-power mobile AR glasses (using the raster path) to high-end holographic workstation displays (using ray traced paths). Because our engine already has a modular `Render` structure and utilizes Slang, sharing the complex 4D interpolation logic between these paths ensures mathematical consistency and visual fidelity. -xref:03_ray_traced_synthesis.adoc[Previous] | xref:../16_Scene_Understanding/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc index 3b0a0a252..5d8343a90 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc @@ -7,4 +7,4 @@ This is the domain of **Scene Understanding**. In this chapter, we will explore By the end of this chapter, you'll understand how to turn the messy physical world into structured geometry that our engine can treat just like any other 3D asset. -xref:../15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Previous] | xref:02_environmental_ingestion.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc index 3fa5619f4..79f4a194a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc @@ -40,4 +40,4 @@ Semantic labels are the "Intelligence Layer" of scene understanding. Instead of By ingesting these meshes directly into our Vulkan buffers, we enable real-time interaction between our shaders and the user's physical room. -xref:01_introduction.adoc[Previous] | xref:03_zero_copy_hand_off.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc index 75b6217bb..d4e4c0a2e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc @@ -42,4 +42,4 @@ In Mixed Reality (MR), users are often viewing the real world through **Passthro Zero-copy hand-offs ensure that sensor data moves at the speed of the GPU's internal bus, keeping the latency below the threshold of human perception and maintaining the illusion that the virtual and physical worlds occupy the same space. -xref:02_environmental_ingestion.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc index d445ae558..39c0807b1 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc @@ -14,7 +14,7 @@ We create a dedicated method, `Engine::updateSpatialMeshes()`, which iterates th // engine.cpp // Supporting Struct: Spatial Mesh Bridge struct XrSpatialMesh { - XrGuidMSFT meshGuid; + XrUuidMSFT meshGuid; std::vector vertices; std::vector indices; glm::mat4 transform; @@ -28,7 +28,7 @@ void Engine::updateSpatialMeshes() { for (const auto& xrMesh : xrMeshes) { // 2. Map the XR mesh ID to an Engine Entity - // We use a internal map: std::unordered_map + // We use a internal map: std::unordered_map Entity* spatialEntity = getOrCreateSpatialEntity(xrMesh.meshGuid); auto* meshComp = spatialEntity->getComponent(); @@ -79,4 +79,4 @@ By mapping spatial data into our existing `Entity` system, we don't need a "spec This "Unified Geometry" approach is the foundation of believable Mixed Reality, allowing us to treat the physical room and our virtual assets as a single, coherent scene. -xref:03_zero_copy_hand_off.adoc[Previous] | xref:../17_ML_Inference_Spatial/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc index b5edd9895..76a55ddfc 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc @@ -7,4 +7,4 @@ Is that moving cluster of vertices a user's hand? Is it a cat jumping on the sof By moving inference from the CPU to the GPU's specialized matrix hardware (like **Tensor Cores** or **Matrix Accelerators**), we can achieve sub-millisecond latency for tasks like hand-pose refinement, semantic segmentation, and mesh reconstruction. We'll learn how to integrate these ML kernels into our engine's compute pipeline, turning noisy sensor data into clean, interactive inputs. -xref:../16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Previous] | xref:02_on_gpu_inference.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc index 82b3bcba6..0f416e0ef 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc @@ -51,4 +51,4 @@ Performing ML inference on the GPU is critical for spatial computing for two pri By integrating ML directly into our spatial compute pipeline, we turn raw, noisy sensor data into clean, interactive inputs with the same low-latency performance as our main rendering path. -xref:01_introduction.adoc[Previous] | xref:03_refining_spatial_data.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc index 9282421cc..c3f3a067f 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc @@ -48,4 +48,4 @@ By refining the spatial data, we move from simple "Visual Passthrough" to true * By using on-GPU ML to polish our spatial data, we bridge the final gap between the messy physical world and the digital precision required for high-end XR. In the next chapter, we will see how to combine these refined meshes with our rendering to achieve **Semantic Occlusion**. -xref:02_on_gpu_inference.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc index 87b262689..7681b1f7c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc @@ -73,4 +73,4 @@ By placing the ML inference pass directly within our `Renderer` and using **Vulk This integration turns our engine from a simple rasterizer into a context-aware spatial platform capable of understanding and reacting to the user's physical environment. -xref:03_refining_spatial_data.adoc[Previous] | xref:../18_Semantic_Occlusion/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc index 612e24992..ee064ab35 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc @@ -7,4 +7,4 @@ In this chapter, we will explore how to bridge the gap between real and virtual By the end of this chapter, you'll be able to implement stable, flicker-free masking that makes virtual assets feel like they truly occupy the same physical space as the user's hands and furniture. -xref:../17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Previous] | xref:02_ml_driven_segmentation.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc index f60b06913..7185b3b36 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc @@ -48,4 +48,4 @@ While semantic labels tell us *what* an object is, they don't always provide its By utilizing on-GPU inference, we generate these masks with minimal latency, ensuring that as the user moves their hand, the virtual world responds and "tucks behind" it instantly. -xref:01_introduction.adoc[Previous] | xref:03_per_pixel_masking.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc index 72755c0d7..927ee640c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc @@ -45,4 +45,4 @@ To fix this, our engine implements **Temporal Reprojection**. We take the mask f By combining semantic understanding with stable per-pixel masking, we create an environment where virtual objects feel physically "locked" into the user's room. -xref:02_ml_driven_segmentation.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc index fbe0acd80..beae72b51 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc @@ -57,4 +57,4 @@ By moving occlusion from a simple "on/off" switch to a dedicated pass in our `Re This pixel-wise awareness is what transforms a "Video Overlay" into a "Spatial Experience," where the boundary between virtual and physical becomes indistinguishable to the user's eye. -xref:03_per_pixel_masking.adoc[Previous] | xref:../19_Platform_Divergence/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc index 92145e4bd..edee3c8b5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc @@ -7,4 +7,4 @@ As an engine developer, you cannot treat these platforms the same. A "standard" In this chapter, we will explore the techniques required to master both platforms. We'll learn how to utilize **Direct Mode** and **HDR10** to saturate high-end GPUs, and how to use **Tile-Based Rendering (TBR)** and **Application SpaceWarp (ASW)** to extract every last drop of performance from mobile silicon. By the end of this chapter, you'll know how to architect your engine's backend to automatically scale its fidelity and efficiency based on the detected spatial hardware. -xref:../18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Previous] | xref:02_desktop_high_end.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc index 57fcbe8d0..79cbff3ff 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc @@ -37,4 +37,4 @@ vk::SwapchainCreateInfoKHR createInfo{ On the desktop, the spatial mindset is about utilizing every watt and every gigabyte of bandwidth to dissolve the boundary between the user and the virtual world. -xref:01_introduction.adoc[Previous] | xref:03_mobile_mastery.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc index 747db4298..60a6c0c36 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc @@ -29,4 +29,4 @@ Mobile headsets use **Unified Memory**, where the CPU and GPU share the same phy To master mobile spatial computing, our engine must be highly surgical with its memory access. Every texture sample and every buffer write must be justified. By focusing on tile-local memory and synthetic frame generation, we can deliver high-quality spatial experiences that run for hours on a single battery charge. -xref:02_desktop_high_end.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc index 844ac11e8..56fd08fd3 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc @@ -61,4 +61,4 @@ By implementing platform-specific presets and Direct Mode handling, we ensure th This architectural flexibility is what allows our spatial applications to scale from the highest possible fidelity to the most efficient mobile standalone performance without rewriting our core rendering logic. -xref:03_mobile_mastery.adoc[Previous] | xref:../20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc index 93665359d..bce3fbb2b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc @@ -9,4 +9,4 @@ In this chapter, we will explore the tools and techniques required to peel back By the end of this chapter, you'll have the diagnostic toolkit required to ship professional-grade spatial applications that are both visually stunning and rock-steady. -xref:../19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Previous] | xref:02_spatial_debugging.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc index 9a0972115..06902d434 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc @@ -36,4 +36,4 @@ In spatial computing, a "Bug" is often not a crash, but a **Dropped Frame**. A s By instrumenting your code with debug labels and utilizing frame analysis tools, you can ensure your spatial experience remains rock-steady and comfortable for the user. -xref:01_introduction.adoc[Previous] | xref:03_automated_qa.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc index d6d305aba..c502087c0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc @@ -33,4 +33,4 @@ Finally, we track **Spatial Performance Metrics** in every build to prevent perf By combining headless simulation, AI-driven visual analysis, and automated performance tracking, we ensure that our spatial engine remains visually stunning, stable, and efficient across every commit and every hardware target. -xref:02_spatial_debugging.adoc[Previous] | xref:04_incorporating_into_the_engine.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc index 024484541..7610298f5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc @@ -60,4 +60,4 @@ By building diagnostic hooks and automated testing directly into the engine, we This "Spatial-First" CI/CD approach is what allows professional teams to maintain high-fidelity experiences across varied hardware targets while keeping the codebase stable and reliable. -xref:03_automated_qa.adoc[Previous] | xref:../conclusion.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/conclusion.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc index 2b89368a3..7958de95b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc @@ -7,4 +7,4 @@ Through the "Incorporating into the Engine" sections at the end of each chapter, Spatial computing is an ever-evolving field, and we encourage you to continue exploring, experimenting, and pushing the boundaries of what's possible with Vulkan 1.4 and OpenXR. -xref:20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Previous] +xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Previous] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc index 1c26e70cb..8f74ef7bf 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc @@ -7,8 +7,8 @@ Welcome to the frontiers of spatial computing. In this tutorial series, we are g Before beginning this tutorial, you should have a solid foundation in both the Vulkan API and engine architecture. Specifically, we assume you have completed: -1. **xref:../00_Introduction.adoc[The Core Vulkan Tutorial]**: You should be comfortable with the core concepts of Vulkan, including instance creation, physical devices, logical devices, and the basic graphics pipeline. -2. **xref:../Building_a_Simple_Engine/introduction.adoc[Building a Simple Engine]**: This series builds directly upon the code and architectural patterns established in the engine tutorial. We will be using the `Simple Engine` codebase as our starting point. +1. **xref:00_Introduction.adoc[The Core Vulkan Tutorial]**: You should be comfortable with the core concepts of Vulkan, including instance creation, physical devices, logical devices, and the basic graphics pipeline. +2. **xref:Building_a_Simple_Engine/introduction.adoc[Building a Simple Engine]**: This series builds directly upon the code and architectural patterns established in the engine tutorial. We will be using the `Simple Engine` codebase as our starting point. 3. **Modern C{pp} and RAII**: We utilize `vulkan-hpp` and RAII (Resource Acquisition Is Initialization) throughout the series. You should be familiar with these patterns to follow the C{pp} integration sections. == The OpenXR Ecosystem: Runtimes and Loaders @@ -54,4 +54,4 @@ By the end of this tutorial, you won't just have a working XR application; you w Let's get started with the first step: the OpenXR-Vulkan 1.4 Handshake. -xref:02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Next] From e251a24f9df09ae33b128d62abcc7848a46b17fd Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 17 Mar 2026 01:19:41 -0700 Subject: [PATCH 06/23] Improve Windows vcpkg setup and error handling in OpenXR engine installation Add vcpkg PATH resolution using VCPKG_INSTALLATION_ROOT. Add error checking after simple_engine and openxr-loader installation steps. Implement vcpkg caching in GitHub Actions workflow to speed up CI builds. Consolidate vcpkg environment setup into separate step with binary cache configuration. --- .github/workflows/openxr_engine_ci.yml | 30 ++++++++++++++++++- .../install_dependencies_windows.bat | 18 +++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openxr_engine_ci.yml b/.github/workflows/openxr_engine_ci.yml index 901130236..f118382fa 100644 --- a/.github/workflows/openxr_engine_ci.yml +++ b/.github/workflows/openxr_engine_ci.yml @@ -121,10 +121,22 @@ jobs: echo "CMAKE_PREFIX_PATH=$SDK_SYSROOT" >> "$GITHUB_ENV" echo "Vulkan_INCLUDE_DIR=$SDK_SYSROOT/include" >> "$GITHUB_ENV" - - name: Bootstrap vcpkg (Windows) + # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. + - name: Cache vcpkg (Windows) if: runner.os == 'Windows' + id: cache-vcpkg-windows + uses: actions/cache@v4 + with: + path: | + ${{ runner.temp }}/vcpkg + ${{ runner.temp }}/vcpkg-cache + key: ${{ runner.os }}-openxr-vcpkg-${{ hashFiles('attachments/simple_engine/vcpkg.json') }} + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' && steps.cache-vcpkg-windows.outputs.cache-hit != 'true' shell: pwsh run: | + $ErrorActionPreference = 'Stop' $vcpkgRoot = Join-Path $env:RUNNER_TEMP "vcpkg" if (-not (Test-Path $vcpkgRoot)) { git clone https://github.com/microsoft/vcpkg $vcpkgRoot @@ -133,7 +145,23 @@ jobs: .\bootstrap-vcpkg.bat Pop-Location "VCPKG_INSTALLATION_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "$vcpkgRoot" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + "CMAKE_TOOLCHAIN_FILE=$vcpkgRoot\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Set vcpkg env (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $vcpkgRoot = Join-Path $env:RUNNER_TEMP "vcpkg" + $vcpkgCache = Join-Path $env:RUNNER_TEMP "vcpkg-cache" + if (-not (Test-Path $vcpkgCache)) { + New-Item -Path $vcpkgCache -ItemType Directory + } + "VCPKG_INSTALLATION_ROOT=$vcpkgRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "$vcpkgRoot" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append "CMAKE_TOOLCHAIN_FILE=$vcpkgRoot\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "VCPKG_DEFAULT_BINARY_CACHE=$vcpkgCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "VCPKG_BINARY_SOURCES=clear;files,$vcpkgCache,readwrite" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Install dependencies (Windows) if: runner.os == 'Windows' diff --git a/attachments/openxr_engine/install_dependencies_windows.bat b/attachments/openxr_engine/install_dependencies_windows.bat index 6d46f0532..8ce0334c1 100644 --- a/attachments/openxr_engine/install_dependencies_windows.bat +++ b/attachments/openxr_engine/install_dependencies_windows.bat @@ -7,12 +7,30 @@ if not exist "%SIMPLE_ENGINE_DIR%" ( exit /b 1 ) +:: Ensure vcpkg is accessible +where vcpkg >nul 2>nul +if %ERRORLEVEL% neq 0 ( + if defined VCPKG_INSTALLATION_ROOT ( + if exist "%VCPKG_INSTALLATION_ROOT%\vcpkg.exe" ( + set "PATH=%VCPKG_INSTALLATION_ROOT%;%PATH%" + ) + ) +) + echo Calling simple_engine dependencies installer... call "%SIMPLE_ENGINE_DIR%\install_dependencies_windows.bat" +if %ERRORLEVEL% neq 0 ( + echo Error: simple_engine dependencies installation failed. + exit /b %ERRORLEVEL% +) echo. echo Installing OpenXR loader... vcpkg install openxr-loader --triplet=x64-windows +if %ERRORLEVEL% neq 0 ( + echo Error: Failed to install openxr-loader. + exit /b %ERRORLEVEL% +) echo. echo OpenXR dependencies installation completed! From baa7f2b04752f02a60975d30743c64a3d030107c Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 17 Mar 2026 01:25:51 -0700 Subject: [PATCH 07/23] Refactor Vulkan physical device property queries to use templated getProperties2/getFeatures2 Replace manual pNext chaining with type-safe templated Vulkan-Hpp methods for querying PhysicalDeviceIDProperties and PhysicalDevicePresentBarrierFeaturesNV. Update code examples in hardware alignment, CAVE architecture, and renderer_core.cpp to use getProperties2<>/getFeatures2<> with compile-time type specification and .get<>() accessor pattern. --- attachments/openxr_engine/renderer_core.cpp | 6 ++---- .../03_hardware_alignment_luid.adoc | 6 ++---- .../05_incorporating_into_the_engine.adoc | 6 ++---- .../12_CAVE_Architecture/03_hardware_sync.adoc | 5 ++--- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index 16320dc50..c362744f7 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -715,10 +715,8 @@ bool Renderer::pickPhysicalDevice() { if (xrMode) { // Match the LUID provided by OpenXR - vk::PhysicalDeviceIDProperties idProps; - vk::PhysicalDeviceProperties2 props2; - props2.pNext = &idProps; - _device.getProperties2(&props2); + auto props2 = _device.getProperties2(); + const auto& idProps = props2.get(); const uint8_t* requiredLuid = xrContext.getRequiredLUID(); if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) { diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc index 4b6cc6d4a..a954b864a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc @@ -40,10 +40,8 @@ To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDPr ---- // Iterating through physical devices to find a match for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { - vk::PhysicalDeviceIDProperties idProps; - vk::PhysicalDeviceProperties2 props2; - props2.pNext = &idProps; - physicalDevice.getProperties2(&props2); + auto props2 = physicalDevice.getProperties2(); + const auto& idProps = props2.get(); if (idProps.deviceLUIDValid) { // Compare the 8-byte LUID against the target from OpenXR diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc index 66bbb7e72..5ccb94af0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc @@ -134,10 +134,8 @@ bool Renderer::pickPhysicalDevice() { for (auto& _device : devices) { if (xrMode) { // Match the LUID provided by OpenXR - vk::PhysicalDeviceIDProperties idProps; - vk::PhysicalDeviceProperties2 props2; - props2.pNext = &idProps; - _device.getProperties2(&props2); + auto props2 = _device.getProperties2(); + const auto& idProps = props2.get(); const uint8_t* requiredLuid = xrContext.getRequiredLUID(); if (requiredLuid && std::memcmp(idProps.deviceLUID, requiredLuid, VK_LUID_SIZE) != 0) { diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc index 6b550e0f1..a9d4c3f76 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc @@ -25,9 +25,8 @@ We use the **VK_NV_present_barrier** extension to join these hardware groups. [source,cpp] ---- // Checking for Present Barrier support and joining a group -vk::PhysicalDevicePresentBarrierFeaturesNV barrierFeatures{}; -vk::PhysicalDeviceFeatures2 features2{ &barrierFeatures }; -physicalDevice.getFeatures2(&features2); +auto features2 = physicalDevice.getFeatures2(); +const auto& barrierFeatures = features2.get(); if (barrierFeatures.presentBarrier) { // When creating the swapchain, we join Barrier 1 From 2aac1f8ee335cc64b0f24df91015fe449b8371e4 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 30 May 2026 21:51:14 -0700 Subject: [PATCH 08/23] Update Vulkan version references from 1.4 to 1.3 throughout Vulkan spatial computing tutorial. --- .../01_introduction.adoc | 6 ++-- .../02_system_integration.adoc | 2 +- .../03_hardware_alignment_luid.adoc | 6 ++-- ...> 04_vulkan_1_3_feature_requirements.adoc} | 20 +++++++------ .../05_incorporating_into_the_engine.adoc | 6 ++-- .../02_external_image_negotiation.adoc | 18 +++++++----- .../03_raii_resource_integration.adoc | 2 +- .../04_memory_ownership_lifecycle.adoc | 2 +- .../04_Dynamic_Rendering/01_introduction.adoc | 2 +- .../06_Late_Latching/01_introduction.adoc | 2 +- .../02_last_second_update.adoc | 2 +- .../02_native_multiview.adoc | 4 +-- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 5 ++-- .../02_fragment_density_control.adoc | 4 +-- .../03_gaze_driven_logic.adoc | 29 +++++++++++++++++++ .../04_incorporating_into_the_engine.adoc | 2 +- .../11_Canted_Displays/01_introduction.adoc | 2 +- .../03_viewport_swizzling.adoc | 2 +- .../12_CAVE_Architecture/01_introduction.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../01_introduction.adoc | 2 +- .../02_on_gpu_inference.adoc | 2 +- .../04_incorporating_into_the_engine.adoc | 2 +- .../02_desktop_high_end.adoc | 17 ++++++----- .../03_mobile_mastery.adoc | 1 + .../conclusion.adoc | 4 +-- .../introduction.adoc | 8 ++--- 28 files changed, 97 insertions(+), 61 deletions(-) rename en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/{04_vulkan_1_4_feature_requirements.adoc => 04_vulkan_1_3_feature_requirements.adoc} (83%) diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc index 80a6add41..543068e72 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc @@ -1,5 +1,5 @@ :pp: {plus}{plus} -= The OpenXR-Vulkan 1.4 Handshake: Introduction += The OpenXR-Vulkan 1.3 Handshake: Introduction Before we can render a single pixel to an XR headset, we must first establish a "handshake" between our application, the OpenXR runtime, and the Vulkan graphics driver. This is not as simple as creating a standard Vulkan instance and passing it to a library; it requires a coordinated dance of extension negotiation and hardware verification. @@ -7,7 +7,7 @@ In this chapter, we are going to look at the three pillars of a successful spati 1. **System Integration**: How to extend our engine's `VulkanContext` to support the mandatory OpenXR extensions, specifically `XR_KHR_vulkan_enable2`. 2. **Hardware Alignment**: Utilizing the Locally Unique Identifier (**LUID**) to ensure that both OpenXR and Vulkan are talking to the exact same physical GPU. This is critical for cross-process memory visibility and performance. -3. **Vulkan 1.4 Requirements**: Activating the modern features—like Timeline Semaphores, Dynamic Rendering, and Synchronization 2—that allow our spatial pipeline to operate with minimal latency. +3. **Vulkan 1.3 Requirements**: Activating the modern features—like Timeline Semaphores, Dynamic Rendering, and Synchronization 2—that allow our spatial pipeline to operate with minimal latency. == The Concept of the Handshake @@ -15,7 +15,7 @@ In a standard desktop application, your engine is the boss. It creates the insta If you don't perform this handshake correctly, you might find that you can't initialize the XR session, or worse, you'll experience massive performance drops as the hardware is forced to copy images between different GPU memory contexts. -== Vulkan 1.4 benefits in OpenXR Application +== Vulkan 1.3 benefits in OpenXR Application * **Low-latency synchronization** between the CPU simulation and the GPU compositor. * **Single-pass rendering** via multiview. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc index 725b6ec38..2869fc7e9 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc @@ -24,7 +24,7 @@ This call populates an `XrGraphicsRequirementsVulkanKHR` structure, which contai 1. **minApiVersionSupported**: The minimum Vulkan version the runtime supports. 2. **maxApiVersionSupported**: The maximum Vulkan version the runtime has been tested with. -Since we are targeting Vulkan 1.4, we must verify that our chosen version falls within this range. Most modern runtimes are rapidly updating to support the latest specifications, ensuring we have access to the advanced synchronization tools we need. +Since we are targeting Vulkan 1.3, we must verify that our chosen version falls within this range. Most modern runtimes are rapidly updating to support the latest specifications, ensuring we have access to the advanced synchronization tools we need. == Extending the Engine Context diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc index a954b864a..360eed15f 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc @@ -56,8 +56,8 @@ for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { The LUID isn't just for selection; it is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our game engine, the images we render must be shared across process boundaries. -Vulkan 1.4 makes this easier by standardizing external memory handles, but these handles only work if the memory was allocated on the same physical silicon. By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies. +Vulkan 1.3 makes this easier by standardizing external memory handles, but these handles only work if the memory was allocated on the same physical silicon. By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies. -In the final part of our handshake, we will ensure that our selected device is configured with the mandatory **Vulkan 1.4 features** required for a modern spatial pipeline. +In the final part of our handshake, we will ensure that our selected device is configured with the mandatory **Vulkan 1.3 features** required for a modern spatial pipeline. -xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc similarity index 83% rename from en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc rename to en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc index 137de6b96..40a0a8109 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc @@ -1,9 +1,11 @@ :pp: {plus}{plus} -= Vulkan 1.4 Feature Requirements += Vulkan 1.3 Feature Requirements -The final step in our spatial handshake is ensuring that our `VkDevice` is initialized with the correct set of features. While OpenXR can technically run on older Vulkan versions, the **Vulkan 1.4** specification is the "gold standard" for modern spatial computing. It codifies several previously optional extensions into the core API, providing a guaranteed baseline of high-performance tools. +The final step in our spatial handshake is ensuring that our `VkDevice` is initialized with the correct set of features. While OpenXR can technically run on older Vulkan versions, the **Vulkan 1.3** specification is the "gold standard" for modern spatial computing. It codifies several previously optional extensions into the core API, providing a guaranteed baseline of high-performance tools. -In this section, we'll look at the three mandatory "pillars" of Vulkan 1.4 that our spatial pipeline depends on. +TIP: For more information on Vulkan 1.3 and core features, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. + +In this section, we'll look at the three mandatory "pillars" of Vulkan 1.3 that our spatial pipeline depends on. == 1. Timeline Semaphores: The Async Heartbeat @@ -29,13 +31,13 @@ If you've followed our engine-building journey, you're already intimately famili * **Dynamic Resolution**: To maintain a steady 90Hz, the engine might need to drop the resolution of peripheral views instantly. * **No Rigid State**: By using `vkCmdBeginRendering` directly on our XR swapchain images, we avoid the heavy overhead and rigid state of legacy `VkRenderPass` and `VkFramebuffer` objects. -== Enabling Vulkan 1.4 Features in RAII +== Enabling Vulkan 1.3 Features in RAII -Vulkan 1.4 simplifies feature enablement. Instead of deep, nested `pNext` chains, we can use the unified `VkPhysicalDeviceVulkan14Features` (and its predecessors for 1.3 and 1.2) to toggle the core requirements. +Vulkan 1.3 simplifies feature enablement. Instead of deep, nested `pNext` chains, we can use the unified `VkPhysicalDeviceVulkan13Features` (and its predecessors for 1.3 and 1.2) to toggle the core requirements. [source,cpp] ---- -// Enabling mandatory features for our spatial pipeline +// Enabling mandatory features for our spatial pipeline, including new Vulkan 1.3 capabilities vk::PhysicalDeviceVulkan13Features features13; features13.dynamicRendering = VK_TRUE; features13.synchronization2 = VK_TRUE; @@ -52,8 +54,8 @@ vk::DeviceCreateInfo createInfo({}, static_cast(deviceExtensions.size()), deviceExtensions.data(), nullptr); // pEnabledFeatures (use pNext instead) -// Ensure we are explicitly asking for Vulkan 1.4 core features if needed -// Note: In Vulkan 1.4, many of these are enabled by default if the version is supported, +// Ensure we are explicitly asking for Vulkan 1.3 core features if needed +// Note: In Vulkan 1.3, many of these are enabled by default if the version is supported, // but explicit enablement is still best practice for engine portability. createInfo.pNext = &features13; @@ -65,7 +67,7 @@ vk::raii::Device device(physicalDevice, createInfo); We have now successfully: 1. Extended our `VulkanContext` to negotiate extensions with OpenXR. 2. Aligned our `VkPhysicalDevice` selection using the hardware LUID. -3. Enabled the modern Vulkan 1.4 features required for low-latency spatial rendering. +3. Enabled the modern Vulkan 1.3 features required for low-latency spatial rendering. With the handshake complete, we are ready to tackle the most significant architectural change in an XR engine: moving from engine-owned swapchains to **Runtime-Owned Swapchains**. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc index 5ccb94af0..670607455 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc @@ -1,7 +1,7 @@ :pp: {plus}{plus} = Incorporating into the Engine -In this chapter, we've explored the fundamental handshake between OpenXR and Vulkan 1.4. To bring these concepts into our `simple_game_engine`, we must evolve our core initialization logic in `renderer_core.cpp` and `xr_context.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp] and link:../../../attachments/openxr_engine/renderer_core.cpp[renderer_core.cpp]. +In this chapter, we've explored the fundamental handshake between OpenXR and Vulkan 1.3. To bring these concepts into our `simple_game_engine`, we must evolve our core initialization logic in `renderer_core.cpp` and `xr_context.cpp`. An example of this implementation can be found in link:../../../attachments/openxr_engine/xr_context.cpp[xr_context.cpp] and link:../../../attachments/openxr_engine/renderer_core.cpp[renderer_core.cpp]. == Testing for XR Mode @@ -147,7 +147,7 @@ bool Renderer::pickPhysicalDevice() { } ---- -=== 3. Enabling Vulkan 1.3/1.4 Features +=== 3. Enabling Vulkan 1.3 Features In `Renderer::createLogicalDevice`, we must explicitly enable the Vulkan 1.3 features required for spatial computing, particularly **Dynamic Rendering** and **Synchronization 2**: @@ -174,4 +174,4 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { By referencing the specific locations in `renderer_core.cpp`, we can see that OpenXR isn't just an "add-on"—it's a **collaborator** in the Vulkan lifecycle. Without the LUID matching in `pickPhysicalDevice`, our engine might initialize on a discrete GPU while the VR headset is tethered to an integrated one, leading to a complete failure to display. Similarly, the extension negotiation ensures that the XR compositor and our engine speak the same "dialect" of Vulkan. -xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Next] +xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc index 3a4082dc5..d1eb70450 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc @@ -38,14 +38,16 @@ Once we have chosen a format, we fill out an `XrSwapchainCreateInfo` structure. [source,cpp] ---- -XrSwapchainCreateInfo createInfo{XR_TYPE_SWAPCHAIN_CREATE_INFO}; -createInfo.format = chosenVulkanFormat; -createInfo.sampleCount = 1; -createInfo.width = viewWidth; -createInfo.height = viewHeight; -createInfo.faceCount = 1; -createInfo.arraySize = 1; // 1 for standard mono/stereo, more for multiview -createInfo.mipCount = 1; +XrSwapchainCreateInfo createInfo{ + .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, + .format = chosenVulkanFormat, + .sampleCount = 1, + .width = viewWidth, + .height = viewHeight, + .faceCount = 1, + .arraySize = 1, // 1 for standard mono/stereo, more for multiview + .mipCount = 1 +}; XrSwapchain xrSwapchain; xrCreateSwapchain(xrSession, &createInfo, &xrSwapchain); diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc index 4201d67dc..ecdf2610c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc @@ -44,7 +44,7 @@ for (const auto& xrImg : xrImages) { == Integrating with Dynamic Rendering -Because we are using **Dynamic Rendering** (introduced in Vulkan 1.3 and core in 1.4), we don't need to wrap these images into `VkFramebuffer` objects. Instead, we can directly reference the `vk::raii::ImageView` in our `vk::RenderingAttachmentInfo`. +Because we are using **Dynamic Rendering** (introduced as an extension and made core in Vulkan 1.3), we don't need to wrap these images into `VkFramebuffer` objects. Instead, we can directly reference the `vk::raii::ImageView` in our `vk::RenderingAttachmentInfo`. [source,cpp] ---- diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc index 3f254f32f..89e712fba 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc @@ -45,7 +45,7 @@ After this call, our application formally "owns" the image at `imageIndex`. This Even though OpenXR manages the ownership, we are still responsible for Vulkan-side synchronization. When an image is returned to us, it might be in a state used by the XR compositor (usually `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` or `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`). -We must use a **Pipeline Barrier** (ideally via **Synchronization 2** in Vulkan 1.4) to transition it to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` before we can use it in our dynamic rendering pass. +We must use a **Pipeline Barrier** (ideally via **Synchronization 2** in Vulkan 1.3) to transition it to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` before we can use it in our dynamic rendering pass. [source,cpp] ---- diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc index 58a9c5993..a9750855e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc @@ -1,7 +1,7 @@ :pp: {plus}{plus} = Dynamic Rendering for Spatial Views -In our journey to integrate OpenXR with Vulkan 1.4, we have established a robust handshake and learned how to manage images that are actually owned by the XR runtime. However, having the images is only half the battle. We now need to render our stereo views into them. In our previous engine building series, we adopted **Dynamic Rendering** (via `VK_KHR_dynamic_rendering`) as our primary rendering path. Now, we'll see why that decision was so critical for spatial computing. +In our journey to integrate OpenXR with Vulkan 1.3, we have established a robust handshake and learned how to manage images that are actually owned by the XR runtime. However, having the images is only half the battle. We now need to render our stereo views into them. In our previous engine building series, we adopted **Dynamic Rendering** (via `VK_KHR_dynamic_rendering`) as our primary rendering path. Now, we'll see why that decision was so critical for spatial computing. == Why Leverage Dynamic Rendering for XR? diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc index 23d91e955..933dc77c8 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc @@ -5,7 +5,7 @@ In spatial computing, the gap between "I think my head is here" and "the pixel i This is where **Late Latching** comes in. Instead of gathering our head pose once at the start of the frame and using it for everything, we want to "latch" onto the most up-to-date pose as late as humanly possible—ideally, right before the GPU begins executing the draw calls that depend on it. -To achieve this in Vulkan 1.4 without causing massive CPU stalls, we leverage **Timeline Semaphores**. Traditionally, synchronizing the CPU and GPU required "heavy" operations like `vkDeviceWaitIdle` or binary semaphores that are difficult to manage across complex frames. Timeline semaphores allow us to create a 64-bit monotonically increasing counter that both the CPU and GPU can wait on or signal. +To achieve this in Vulkan 1.3 without causing massive CPU stalls, we leverage **Timeline Semaphores**. Traditionally, synchronizing the CPU and GPU required "heavy" operations like `vkDeviceWaitIdle` or binary semaphores that are difficult to manage across complex frames. Timeline semaphores allow us to create a 64-bit monotonically increasing counter that both the CPU and GPU can wait on or signal. By gating our Uniform Buffer Object (UBO) or Push Constant updates behind a timeline semaphore value, we can ensure that the GPU only proceeds with a draw call once the CPU has finished writing the absolute latest view and projection matrices. This minimizes the prediction error and keeps the virtual world anchored firmly to the user's reality. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc index 25751597a..dc8688b19 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc @@ -1,7 +1,7 @@ :pp: {plus}{plus} = The Last-Second Update -To implement late latching, we need a mechanism that allows the GPU to sit in a "ready state" for as long as possible without stalling the entire hardware pipeline. In Vulkan 1.4, the combination of **Timeline Semaphores** and **Synchronization 2** provides exactly what we need. +To implement late latching, we need a mechanism that allows the GPU to sit in a "ready state" for as long as possible without stalling the entire hardware pipeline. In Vulkan 1.3, the combination of **Timeline Semaphores** and **Synchronization 2** provides exactly what we need. == Gating the Pipeline: The Wait-Before-Submit Pattern diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc index b8471e82a..0244eab3a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc @@ -17,7 +17,7 @@ In a complex scene, this means the CPU is doing twice the work (4,000 draw calls == View Masks and the Broadcast Mechanism -In a standard Vulkan pipeline, a draw call targets a specific attachment. With multiview enabled (part of the Vulkan 1.4 core), you specify a **View Mask** in your `vk::RenderingInfo`. +In a standard Vulkan pipeline, a draw call targets a specific attachment. With multiview enabled (part of the Vulkan 1.3 core), you specify a **View Mask** in your `vk::RenderingInfo`. A view mask is a bitmask where each set bit represents a layer in the output attachment array. @@ -27,7 +27,7 @@ A view mask is a bitmask where each set bit represents a layer in the output att [source,cpp] ---- -// 1. Enabling Multiview in Vulkan 1.4 Dynamic Rendering +// 1. Enabling Multiview in Vulkan 1.3 Dynamic Rendering vk::RenderingInfo renderingInfo; renderingInfo.viewMask = 0b11; // Broadcast to bit 0 and bit 1 ---- diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc index 10c7d8ac1..9c1a815fd 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc @@ -71,6 +71,6 @@ VS_Output vertexMain(VS_Input input, uint viewID : SV_ViewID) { == Why These Changes? -By enabling the `viewMask` and using `SV_ViewID`, we shift the burden of stereo rendering from the CPU to the GPU. Instead of the engine looping twice and submitting two sets of commands, it submits one set, and the hardware's **Instance Data Step** or **Geometry Replication** handles the expansion to two views. This is the most efficient way to achieve high-performance spatial rendering in Vulkan 1.4. +By enabling the `viewMask` and using `SV_ViewID`, we shift the burden of stereo rendering from the CPU to the GPU. Instead of the engine looping twice and submitting two sets of commands, it submits one set, and the hardware's **Instance Data Step** or **Geometry Replication** handles the expansion to two views. This is the most efficient way to achieve high-performance spatial rendering in Vulkan 1.3. xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc index a6bf34e87..c0da2bd99 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc @@ -3,14 +3,15 @@ The core challenge of high-fidelity spatial computing is the sheer number of pixels we need to push. With headsets moving toward 4K-per-eye resolutions at 90Hz or 120Hz, simply rendering the entire scene at full resolution is often computationally prohibitive. However, human vision provides us with a unique optimization opportunity: **Foveated Rendering**. -Our eyes only perceive high detail in a very small central area called the **fovea**. As we move toward the periphery of our vision, our ability to resolve fine detail drops off significantly. In this chapter, we will explore how to leverage **Variable Rate Shading (VRS)**—a core feature of Vulkan 1.4—to intelligently reduce fragment processing in these peripheral areas without sacrificing perceived quality. +Our eyes only perceive high detail in a very small central area called the **fovea**. As we move toward the periphery of our vision, our ability to resolve fine detail drops off significantly. In this chapter, we will explore how to leverage **Variable Rate Shading (VRS)**—a powerful Vulkan extension—to intelligently reduce fragment processing in these peripheral areas without sacrificing perceived quality. Variable Rate Shading allows us to decoupling the shading rate from the pixel rate. Instead of running a fragment shader once for every pixel, we can tell the hardware to run it once for a group of pixels (e.g., a 2x2 or 4x4 tile). This "coarse shading" significantly reduces the **ALU** (Arithmetic Logic Unit) load on the GPU, which is often the primary bottleneck in complex spatial shaders. We will focus on two primary strategies: + 1. **Static Peripheral Optimization**: Reducing shading rates at the edges of the lens where optical distortion and chromatic aberration already obscure detail. 2. **Dynamic Gaze-Driven Shading**: Using eye-tracking telemetry to center the high-resolution region wherever the user is currently looking. -By the end of this chapter, you will understand how to integrate the **VK_KHR_fragment_shading_rate** extension (now part of the Vulkan 1.4 core) into your spatial pipeline and how to manage shading rate maps that update in real-time. +By the end of this chapter, you will understand how to integrate the **VK_KHR_fragment_shading_rate** extension (or its mobile counterpart **VK_EXT_fragment_density_map**) into your spatial pipeline and how to manage shading rate maps that update in real-time. xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc index 655784e75..bdb7098fe 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc @@ -1,7 +1,7 @@ :pp: {plus}{plus} -= Fragment Density Control and Vulkan 1.4 VRS += Fragment Density Control and VRS Extensions -In legacy pipelines, reducing shading quality often meant rendering to a lower-resolution buffer and upscaling, which introduced blurring and aliasing artifacts. With Vulkan 1.4, we have a much more elegant solution: **Variable Rate Shading (VRS)**. +In legacy pipelines, reducing shading quality often meant rendering to a lower-resolution buffer and upscaling, which introduced blurring and aliasing artifacts. Modern Vulkan provides a much more elegant solution via **Variable Rate Shading (VRS)** extensions. == The Concept: Decoupling Rasterization from Shading diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc index 1f37abb5e..a33958132 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc @@ -28,7 +28,36 @@ void updateShadingRateMap(vk::raii::CommandBuffer& cmd, const glm::vec2& gazeCen cmd.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eCompute, 0, gazeCenter); cmd.dispatch(shadingRateWidth / 8, shadingRateHeight / 8, 1); } +----- + +[source,glsl] ---- +// Simple Compute Shader to generate a Gaze-Driven Shading Rate Map +layout(local_size_x = 8, local_size_y = 8) in; +layout(set = 0, binding = 0, r8ui) uniform writeonly uimage2D shadingRateImage; + +layout(push_constant) uniform PushConstants { + vec2 gazeCenter; // UV coordinates of the eye gaze +}; + +void main() { + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + vec2 uv = vec2(pos) / vec2(imageSize(shadingRateImage)); + + float dist = distance(uv, gazeCenter); + + // Shading Rate values: 0 = 1x1, 4 = 2x1, 5 = 2x2 + uint rate = 0; // 1x1 (Full Quality) + if (dist > 0.35) { + rate = 5; // 2x2 (Coarse) + } else if (dist > 0.15) { + rate = 4; // 2x1 (Medium) + } + + imageStore(shadingRateImage, pos, uvec4(rate)); +} +---- + == Latency: The Immersion Killer diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc index ebec77e0c..dc921fb14 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc @@ -77,6 +77,6 @@ void Renderer::updateShadingRateImage(glm::vec2 gazeNDC) { == Why These Changes? -By integrating eye-tracking data directly into our Vulkan 1.4 rendering pass, we enable **Dynamic Foveated Rendering (DFR)**. This allows the engine to save up to 60% of fragment processing costs in complex PBR scenes by only rendering the tiny area the user is actually looking at in full detail. Because our engine uses dynamic rendering, adding the `vrsAttachment` to our `passInfo` is a simple `pNext` extension away. +By integrating eye-tracking data directly into our Vulkan 1.3 rendering pass, we enable **Dynamic Foveated Rendering (DFR)**. This allows the engine to save up to 60% of fragment processing costs in complex PBR scenes by only rendering the tiny area the user is actually looking at in full detail. Because our engine uses dynamic rendering, adding the `vrsAttachment` to our `passInfo` is a simple `pNext` extension away. xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc index 3185c4923..d785a1d4a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc @@ -8,7 +8,7 @@ This mechanical canting introduces a significant layer of complexity to our rend In this chapter, we will explore how to handle these non-parallel architectures by: 1. **Decomposing the XR Pose**: Understanding how to use `XrView` data to build proper 4x4 matrices that account for the canting angle. 2. **Calculating Asymmetric Frustums**: Using the OpenXR `fov` tangents to build projection matrices where the "center" is shifted to align with the optical axis. -3. **Optimizing with Viewport Swizzling**: Utilizing the **VK_NV_viewport_swizzle** extension (or its Vulkan 1.4 equivalents) to optimize how these non-rectilinear projections are rasterized. +3. **Optimizing with Viewport Swizzling**: Utilizing the **VK_NV_viewport_swizzle** extension (or its Vulkan 1.3 equivalents) to optimize how these non-rectilinear projections are rasterized. By mastering these concepts, your engine will be compatible with the entire spectrum of spatial hardware, from mobile VR headsets with parallel screens to ultra-wide FOV enthusiast hardware. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc index fa0f1bd89..898de3273 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc @@ -1,7 +1,7 @@ :pp: {plus}{plus} = Optimizing Projections with Viewport Swizzling -In some wide-FOV headsets, the physical display panel may be mounted at an angle that does not align with the standard Cartesian coordinate system. This is where the **VK_NV_viewport_swizzle** extension (or the modern Vulkan 1.4 equivalents) becomes invaluable. +In some wide-FOV headsets, the physical display panel may be mounted at an angle that does not align with the standard Cartesian coordinate system. This is where the **VK_NV_viewport_swizzle** extension becomes invaluable. == The Swizzle Stage: Re-routing the Pipeline diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc index c9956a5f3..e7dea2722 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc @@ -21,7 +21,7 @@ Furthermore, because the images are being stitched together from multiple projec The "holy grail" of CAVE engineering is **Genlock** (Generator Lock) and **Swaplock**. Genlock ensures that the video signal from every GPU is sent to the projectors at the exact same time. Swaplock ensures that the command to swap the front and back buffers happens simultaneously across the entire cluster. -Without these hardware-level guarantees, the "immersion" of a CAVE is quickly broken by jitter and visual discontinuities. We will look at how Vulkan 1.4 features and specialized hardware extensions allow us to achieve this level of precision. +Without these hardware-level guarantees, the "immersion" of a CAVE is quickly broken by jitter and visual discontinuities. We will look at how Vulkan 1.3 features and specialized hardware extensions allow us to achieve this level of precision. By the end of this chapter, you will understand how to scale your spatial engine from a single-user headset to a multi-node, room-scale immersive environment. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc index 51ed0f0ce..a0e388a98 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc @@ -15,7 +15,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { vk::PhysicalDeviceVulkan12Features vulkan12Features; vulkan12Features.bufferDeviceAddress = vk::True; - // NEW: Also enable 1.4 features for easier memory management + // NEW: Also enable 1.3 features for easier memory management vk::PhysicalDeviceVulkan13Features vulkan13Features; vulkan13Features.dynamicRendering = vk::True; vulkan13Features.pNext = &vulkan12Features; diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc index 76a55ddfc..62d9fcd79 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc @@ -3,7 +3,7 @@ As we've seen in the previous chapter, the sensors on a modern XR headset provide a flood of raw physical data. But capturing triangles is only half the battle. To create truly intelligent spatial applications, we need to "understand" that data in real-time. -Is that moving cluster of vertices a user's hand? Is it a cat jumping on the sofa? To answer these questions without stalling our rendering pipeline, we turn to **ML Inference** (Machine Learning). In this chapter, we'll explore how to leverage the **Cooperative Matrices** in Vulkan 1.4 to perform high-speed neural network calculations directly on the GPU. +Is that moving cluster of vertices a user's hand? Is it a cat jumping on the sofa? To answer these questions without stalling our rendering pipeline, we turn to **ML Inference** (Machine Learning). In this chapter, we'll explore how to leverage the **Cooperative Matrices** in Vulkan 1.3 to perform high-speed neural network calculations directly on the GPU. By moving inference from the CPU to the GPU's specialized matrix hardware (like **Tensor Cores** or **Matrix Accelerators**), we can achieve sub-millisecond latency for tasks like hand-pose refinement, semantic segmentation, and mesh reconstruction. We'll learn how to integrate these ML kernels into our engine's compute pipeline, turning noisy sensor data into clean, interactive inputs. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc index 0f416e0ef..f1ec2d353 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc @@ -21,7 +21,7 @@ By performing this inference on the GPU, we can use **GEMM** (General Matrix Mul [source,slang] ---- // Using Slang's Matrix fragments for cooperative ML inference -// Note: Requires Vulkan 1.4 with VK_KHR_cooperative_matrix +// Note: Requires Vulkan 1.3 with VK_KHR_cooperative_matrix [shader("compute")] [numthreads(32, 1, 1)] // Subgroup-sized for optimal matrix math void performHandRefinement() { diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc index 7681b1f7c..bde4abacf 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc @@ -69,7 +69,7 @@ std::shared_ptr ResourceManager::loadMLWeights(const std::string& path) == Why These Changes? -By placing the ML inference pass directly within our `Renderer` and using **Vulkan 1.4** synchronization (Synchronization 2), we ensure that our spatial refinement happens at the absolute last microsecond before the data is needed. This minimizes the **Motion-to-Photon** gap. Furthermore, by treating weights as standard engine resources, we can hot-reload different ML models (e.g., switching from a high-performance "Mobile" model to a high-fidelity "Desktop" model) without restarting the engine. +By placing the ML inference pass directly within our `Renderer` and using **Vulkan 1.3** synchronization (Synchronization 2), we ensure that our spatial refinement happens at the absolute last microsecond before the data is needed. This minimizes the **Motion-to-Photon** gap. Furthermore, by treating weights as standard engine resources, we can hot-reload different ML models (e.g., switching from a high-performance "Mobile" model to a high-fidelity "Desktop" model) without restarting the engine. This integration turns our engine from a simple rasterizer into a context-aware spatial platform capable of understanding and reacting to the user's physical environment. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc index 79cbff3ff..e92f4b3fb 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc @@ -14,17 +14,10 @@ We use **Direct Mode**. This allows the XR runtime to take exclusive control of == HDR10 and 10-bit Color: Beyond the Screen -Desktop GPUs have the bandwidth to support **High Dynamic Range (HDR)**. In XR, this is essential for physical realism. A virtual sun should be thousands of times brighter than a virtual candle. Without HDR, the engine must "tone-map" these values into the same narrow range, losing the feeling of scale. +While mobile platforms often have excellent HDR support, desktop GPUs allow us to leverage **High Dynamic Range (HDR)** at much higher internal resolutions and bit-depths. In XR, this is essential for physical realism. A virtual sun should be thousands of times brighter than a virtual candle. Without HDR, the engine must "tone-map" these values into the same narrow range, losing the feeling of scale. To implement this, our `VulkanContext` negotiates a 10-bit color format—such as `VK_FORMAT_A2B10G10R10_UNORM_PACK32`—with the OpenXR runtime. This provides over a billion colors, eliminating "banding" artifacts in dark spatial environments like night scenes or caves. -== Leveraging High PCIe Bandwidth - -Because we have a high-speed **PCIe 4.0** or **5.0** link between the CPU and GPU, we can push massive amounts of data per frame without bottlenecking. - -* **Uncompressed LightFields**: We can upload gigabytes of plenoptic data directly to the GPU without stalling the render loop. -* **Ray Tracing**: We can afford the high overhead of building **Acceleration Structures** (AS) for complex, high-poly environments every frame, enabling real-time spatial reflections and global illumination. - [source,cpp] ---- // Configuring HDR10 for high-end desktop XR with vulkan-hpp @@ -35,6 +28,14 @@ vk::SwapchainCreateInfoKHR createInfo{ }; ---- +== Leveraging High PCIe Bandwidth + +With **PCIe 4.0** or **5.0** links, we have high-speed data transfer between the CPU and GPU, which is vital for streaming complex spatial assets. However, at the high frame rates required for XR, even these links can become a bottleneck if not managed carefully—especially on systems with limited PCIe lanes. + +* **Uncompressed LightFields**: We can upload gigabytes of plenoptic data directly to the GPU without stalling the render loop. +* **Ray Tracing**: We can afford the high overhead of building **Acceleration Structures** (AS) for complex, high-poly environments every frame, enabling real-time spatial reflections and global illumination. + + On the desktop, the spatial mindset is about utilizing every watt and every gigabyte of bandwidth to dissolve the boundary between the user and the virtual world. xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc index 60a6c0c36..b7a7156b0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc @@ -13,6 +13,7 @@ In mobile spatial computing, **Discarding is Winning**. 1. **Load Op**: Does the tile need to be cleared (`eClear`) or loaded from VRAM (`eLoad`)? 2. **Store Op**: Does the final color need to be written back to VRAM (`eStore`) or discarded (`eDontCare`)? +3. **Local Read**: In Vulkan 1.3, we can use the **Dynamic Rendering Local Read** extension (`VK_KHR_dynamic_rendering_local_read`) to read back color or depth values within the same tile without hitting main VRAM. This is essential for mobile-focused techniques like deferred shading or custom blending, keeping the entire operation on-chip. If you don't need the depth buffer for the next frame, you MUST set its store op to `eDontCare`. This prevents the GPU from wasting battery power moving megabytes of depth data across the bus, which is the most power-hungry operation on a mobile chip. diff --git a/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc index 7958de95b..839d743b8 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/conclusion.adoc @@ -1,10 +1,10 @@ :pp: {plus}{plus} = Conclusion -Congratulations on completing the **OpenXR and Vulkan 1.4 Spatial Computing** tutorial series! We've covered a vast amount of ground, from the initial handshake between the two APIs to advanced topics like LightField Theory and Plenoptic Synthesis. +Congratulations on completing the **OpenXR and Vulkan 1.3 Spatial Computing** tutorial series! We've covered a vast amount of ground, from the initial handshake between the two APIs to advanced topics like LightField Theory and Plenoptic Synthesis. Through the "Incorporating into the Engine" sections at the end of each chapter, you've seen how to take these theoretical concepts and apply them to a real-world renderer like our `simple_game_engine`. -Spatial computing is an ever-evolving field, and we encourage you to continue exploring, experimenting, and pushing the boundaries of what's possible with Vulkan 1.4 and OpenXR. +Spatial computing is an ever-evolving field, and we encourage you to continue exploring, experimenting, and pushing the boundaries of what's possible with Vulkan 1.3 and OpenXR. xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Previous] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc index 8f74ef7bf..430e98ffa 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/introduction.adoc @@ -1,5 +1,5 @@ :pp: {plus}{plus} -= OpenXR and Vulkan 1.4 Spatial Computing: Introduction += OpenXR and Vulkan 1.3 Spatial Computing: Introduction Welcome to the frontiers of spatial computing. In this tutorial series, we are going to bridge the gap between traditional 2D engine architecture and the immersive, low-latency requirements of Virtual Reality (VR), Augmented Reality (AR), and Mixed Reality (MR)—collectively known as XR. @@ -31,9 +31,9 @@ In the early days of XR, developers had to write custom backends for every heads But OpenXR isn't just a wrapper. It is a sophisticated state machine that manages hardware poses, predictive frame timing, and specialized swapchains that are owned by the XR runtime itself. To use it effectively, we have to rethink how our engine handles its main loop, its memory, and its synchronization. -== The Vulkan 1.4 Advantage +== The Vulkan 1.3 Advantage -Vulkan 1.4 brings several critical features to the table that are particularly powerful for spatial computing: +Vulkan 1.3 brings several critical features to the table that are particularly powerful for spatial computing: * **Timeline Semaphores**: Essential for the complex, cross-process synchronization required between your engine and the XR compositor. * **Dynamic Rendering**: Provides a flexible, lightweight rendering path for stereo views, avoiding the rigid state of legacy Render Passes. @@ -52,6 +52,6 @@ Throughout this series, we will cover the entire lifecycle of an XR frame: By the end of this tutorial, you won't just have a working XR application; you will have a deep understanding of the architectural patterns required for high-performance spatial computing in the modern Vulkan ecosystem. -Let's get started with the first step: the OpenXR-Vulkan 1.4 Handshake. +Let's get started with the first step: the OpenXR-Vulkan 1.3 Handshake. xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Next] From fc69a205c669c7bf2905a4e9cc0a12a09928483f Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 30 May 2026 22:11:58 -0700 Subject: [PATCH 09/23] update the nav to 1.3 instead of 1.4 --- antora/modules/ROOT/nav.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 19625144b..7d46b25f2 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -150,13 +150,13 @@ ** Appendix *** xref:Building_a_Simple_Engine/Appendix/appendix.adoc[Appendix] -* OpenXR and Vulkan 1.4 Spatial Computing +* OpenXR and Vulkan 1.3 Spatial Computing ** xref:OpenXR_Vulkan_Spatial_Computing/introduction.adoc[Introduction] -** The OpenXR-Vulkan 1.4 Handshake +** The OpenXR-Vulkan 1.3 Handshake *** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Introduction] *** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[System Integration] *** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Hardware Alignment (LUID)] -*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_4_feature_requirements.adoc[Vulkan 1.4 Feature Requirements] +*** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Vulkan 1.3 Feature Requirements] *** xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc[Incorporating into the Engine] ** Runtime-Owned Swapchains *** xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Introduction] From 8d79a9a29ca1c97ff143a750529ac8414378ab18 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 31 May 2026 00:32:57 -0700 Subject: [PATCH 10/23] fix code listing begin/end --- .../10_Variable_Rate_Shading/03_gaze_driven_logic.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc index a33958132..18df42f86 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc @@ -28,7 +28,7 @@ void updateShadingRateMap(vk::raii::CommandBuffer& cmd, const glm::vec2& gazeCen cmd.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eCompute, 0, gazeCenter); cmd.dispatch(shadingRateWidth / 8, shadingRateHeight / 8, 1); } ------ +---- [source,glsl] ---- From 987ccb3c49768182b3ed38d52b82c998737551bb Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 31 May 2026 00:42:30 -0700 Subject: [PATCH 11/23] Migrate from OpenXR C++ bindings to C API and fix CMake build configuration Replace openxr.hpp C++ wrapper with native openxr.h C API throughout xr_context. Add OpenXR::OpenXR alias for openxr_loader target compatibility. Fix memory_pool.cpp source path and Assets copy destination. Load Vulkan extension function pointers explicitly via xrGetInstanceProcAddr. Implement proper LUID extraction from OpenXR-selected physical device using VkPhysicalDeviceIDProperties. Replace XrGuidMSFT with XrUuidMSFT for spatial mesh structure. Update all OpenXR handle types from C++ wrappers to native C types (XrInstance, XrSession, XrSpace, XrAction, XrSwapchain). Convert all OpenXR API calls from method-style to function-style. Initialize views vector with proper XR_TYPE_VIEW structure type. --- attachments/openxr_engine/shaders/pbr.slang | 14 ++++++-------- .../openxr_engine/shaders/texturedMesh.slang | 2 -- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/attachments/openxr_engine/shaders/pbr.slang b/attachments/openxr_engine/shaders/pbr.slang index 83c705e2e..38dc76386 100644 --- a/attachments/openxr_engine/shaders/pbr.slang +++ b/attachments/openxr_engine/shaders/pbr.slang @@ -44,7 +44,6 @@ struct VSOutput { float3 GeometricNormal : NORMAL1; float2 UV : TEXCOORD0; float4 Tangent : TANGENT; - uint ViewID : SV_ViewID; }; [[vk::binding(0, 1)]] Sampler2D opaqueSceneColor; @@ -112,7 +111,6 @@ bool traceShadowOccluded(float3 origin, float3 direction, float tMin, float tMax VSOutput VSMain(VSInput input, uint viewID : SV_ViewID) { VSOutput output; - output.ViewID = viewID; float4x4 instanceModelMatrix = input.InstanceModelMatrix; float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); output.Position = mul(ubo.viewProjections[viewID], worldPos); @@ -137,7 +135,7 @@ VSOutput VSMain(VSInput input, uint viewID : SV_ViewID) // Fragment shader entry point for generic PBR materials [[shader("fragment")]] -float4 PSMain(VSOutput input) : SV_TARGET +float4 PSMain(VSOutput input, uint viewID : SV_ViewID) : SV_TARGET { // --- 1. Material Properties --- float2 uv = float2(input.UV.x, 1.0 - input.UV.y); @@ -196,7 +194,7 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 G = normalize(input.GeometricNormal); // --- 3. Opaque Lighting Calculation --- - float3 V = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + float3 V = normalize(ubo.camPoses[viewID].xyz - input.WorldPos); float3 diffuseLighting = float3(0.0, 0.0, 0.0); float3 specularLighting = float3(0.0, 0.0, 0.0); @@ -214,7 +212,7 @@ float4 PSMain(VSOutput input) : SV_TARGET uint totalTiles = max(tilesX * tilesY, 1u); // Clustered Z slice index from view-space depth (positive distance) - float dVS = abs(mul(ubo.views[input.ViewID], float4(input.WorldPos, 1.0)).z); + float dVS = abs(mul(ubo.views[viewID], float4(input.WorldPos, 1.0)).z); float lnN = log(max(ubo.nearZ, 1e-4)); float lnF = log(max(ubo.farZ, lnN + 1e-4)); float denom = max(lnF - lnN, 1e-6); @@ -408,7 +406,7 @@ float4 PSMain(VSOutput input) : SV_TARGET // lighting and screen-space reflections are omitted to avoid global // bright/dark flashes across large glass surfaces. [[shader("fragment")]] -float4 GlassPSMain(VSOutput input) : SV_TARGET +float4 GlassPSMain(VSOutput input, uint viewID : SV_ViewID) : SV_TARGET { // --- 1. Material / texture sampling (minimal subset) --- float2 uv = float2(input.UV.x, 1.0 - input.UV.y); @@ -433,7 +431,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET // Geometric normal for view-angle dependence and refraction float3 G = normalize(input.GeometricNormal); - float3 V = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + float3 V = normalize(ubo.camPoses[viewID].xyz - input.WorldPos); // Base albedo used for transmission tint float3 albedo = baseColor.rgb; @@ -551,7 +549,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET } if (count > 0u) { float3 Ng = normalize(input.GeometricNormal); - float3 Vv = normalize(ubo.camPoses[input.ViewID].xyz - input.WorldPos); + float3 Vv = normalize(ubo.camPoses[viewID].xyz - input.WorldPos); // Use a neutral albedo to avoid darkening glass; weight specular more float3 alb = float3(0.6, 0.6, 0.6); float rough = 0.49; diff --git a/attachments/openxr_engine/shaders/texturedMesh.slang b/attachments/openxr_engine/shaders/texturedMesh.slang index 7c2595734..c674cb74c 100644 --- a/attachments/openxr_engine/shaders/texturedMesh.slang +++ b/attachments/openxr_engine/shaders/texturedMesh.slang @@ -40,7 +40,6 @@ struct VSOutput { float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; float4 Tangent : TANGENT; // Pass through tangent to satisfy validation layer - uint ViewID : SV_ViewID; }; // Uniform buffer (matched to renderer.h) @@ -58,7 +57,6 @@ struct UniformBufferObject { VSOutput VSMain(VSInput input, uint viewID : SV_ViewID) { VSOutput output; - output.ViewID = viewID; // Use instance matrices directly (CPU uploads column-major model // matrix and three float4 normal-matrix columns in attributes From dd10eda1c08647848c4ca9aeee6dd3422fdc9496 Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 11 Jun 2026 16:49:54 -0700 Subject: [PATCH 12/23] Update for Rylie's and OpenXR WG suggestions. Also update for Sascha's suggestions. --- .../02_system_integration.adoc | 60 ++++++++----- .../03_hardware_alignment_luid.adoc | 36 +++++--- .../04_vulkan_1_3_feature_requirements.adoc | 64 +++++++------- .../05_incorporating_into_the_engine.adoc | 21 +++-- .../02_external_image_negotiation.adoc | 30 ++++--- .../03_raii_resource_integration.adoc | 70 ++++++++++------ .../04_memory_ownership_lifecycle.adoc | 80 +++++++++++------- .../02_rendering_to_spatial_swapchains.adoc | 84 +++++++++++-------- .../03_stereo_viewport_scissor.adoc | 57 ++++++++----- .../02_xr_lifecycle.adoc | 47 +++++++---- .../03_display_time_prediction.adoc | 40 +++++---- .../02_last_second_update.adoc | 69 ++++++++++----- .../06_Late_Latching/03_implementation.adoc | 67 ++++++++------- .../02_openxr_action_system.adoc | 83 +++++++++--------- .../03_space_manifolds.adoc | 45 ++++++---- .../02_native_multiview.adoc | 54 +++++++----- .../03_slang_architecture.adoc | 56 +++++++++---- .../02_primary_stereo_with_insets.adoc | 54 +++++++----- .../03_multi_layer_composition.adoc | 71 +++++++++++----- .../02_fragment_density_control.adoc | 60 +++++++------ .../03_gaze_driven_logic.adoc | 30 ++++--- .../02_non_parallel_projections.adoc | 41 ++++++--- .../02_projector_based_spatial_tech.adoc | 45 +++++++--- .../02_geometric_correction.adoc | 80 ++++++++++-------- .../03_post_process_warping.adoc | 53 +++++++++--- .../02_4d_lightfield_representation.adoc | 44 +++++----- .../03_high_density_view_arrays.adoc | 47 ++++++----- .../02_synthesis_shaders.adoc | 62 ++++++++++---- .../03_ray_traced_synthesis.adoc | 54 +++++++++--- .../02_environmental_ingestion.adoc | 41 ++++++--- .../03_zero_copy_hand_off.adoc | 39 ++++++--- .../02_on_gpu_inference.adoc | 52 +++++++----- .../03_refining_spatial_data.adoc | 60 +++++++++---- .../02_ml_driven_segmentation.adoc | 62 +++++++++----- .../03_per_pixel_masking.adoc | 52 +++++++----- .../02_desktop_high_end.adoc | 4 +- .../03_mobile_mastery.adoc | 43 +++++++--- .../02_spatial_debugging.adoc | 46 ++++++---- .../03_automated_qa.adoc | 46 ++++++---- 39 files changed, 1317 insertions(+), 732 deletions(-) diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc index 2869fc7e9..c65d62ab2 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc @@ -3,21 +3,27 @@ In a standard desktop application, our engine is typically responsible for creating a `VkInstance` and selecting a `VkPhysicalDevice` based on its own internal logic. When we move to OpenXR, we are no longer the sole decision-maker. We must first negotiate with the XR runtime to ensure our graphics context is compatible with the headset's compositor. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Connecting your Vulkan engine to an XR runtime is a mandatory process defined by the OpenXR specification. You must perform this coordinated handshake to ensure that your application and the runtime can share resources efficiently and safely. + The bridge between OpenXR and Vulkan is the extension **XR_KHR_vulkan_enable2**. This extension provides the mechanism for OpenXR to specify exactly which instance and device extensions are required, as well as any specific feature flags that must be enabled for the spatial pipeline to function. == The Architectural Boundary: Why a Handshake? -To understand why this negotiation is necessary, we must first understand the architectural boundary between our engine and the **OpenXR Runtime**. Unlike a standard windowing system, an XR runtime (like SteamVR, Meta Link, or Monado) often operates as a separate, high-priority system service or process. +To understand why this negotiation is necessary, we must look at what the **OpenXR Runtime** (like SteamVR, Meta Link, or Monado) is doing behind the scenes. Unlike a standard windowing system, an XR runtime often operates as a separate, high-priority system service or process. -1. **Process Isolation**: The runtime manages the physical hardware (cameras, IMUs, and displays) and performs critical tasks like **Asynchronous Reprojection**—warping the final image if our engine misses its frame deadline. -2. **VRAM Sharing**: Because the runtime and our engine are separate processes, they must share access to the GPU's memory. This isn't just a simple copy; it is a "Zero-Copy" handoff where we both see the same physical memory addresses. -3. **Synchronization**: We need a way to tell the runtime "I am done rendering this frame" and for the runtime to tell us "I am done using this image, you can have it back." +* **Process Isolation**: The runtime manages the physical hardware (cameras, IMUs, and displays) and performs critical tasks like **Asynchronous Reprojection**—warping the final image if our engine misses its frame deadline. +* **VRAM Sharing**: Because the runtime and our engine are separate processes, they must share access to the GPU's memory. This is a "Zero-Copy" handoff where we both see the same physical memory addresses. +* **Validation**: When you call `xrCreateSession`, the runtime validates that the `VkInstance` and `VkDevice` you provided were created with the extensions it requested. The handshake is our way of telling the OS and the XR runtime: "We are both speaking the same dialect of Vulkan, using the same extensions, and looking at the same physical silicon." == Querying XR Graphics Requirements -Before we can even create our Vulkan instance, we must initialize our OpenXR instance and query the graphics requirements for the specific system (the headset) we are targeting. This is performed using the `xrGetVulkanGraphicsRequirements2KHR` function. +Before we can create our Vulkan instance, we must initialize our OpenXR instance and query the graphics requirements for the specific system (the headset) we are targeting using `xrGetVulkanGraphicsRequirements2KHR`. This call populates an `XrGraphicsRequirementsVulkanKHR` structure, which contains two critical pieces of information: @@ -26,23 +32,16 @@ This call populates an `XrGraphicsRequirementsVulkanKHR` structure, which contai Since we are targeting Vulkan 1.3, we must verify that our chosen version falls within this range. Most modern runtimes are rapidly updating to support the latest specifications, ensuring we have access to the advanced synchronization tools we need. -== Extending the Engine Context +== Coordinated Engine Initialization Our engine's initialization logic needs to be modified to accept these external requirements. Instead of hardcoding a static list of instance extensions, we perform a coordinated handshake: 1. **Query OpenXR**: Ask the runtime for its mandatory instance extensions using `xrGetVulkanInstanceExtensionsKHR`. 2. **Merge and Initialize**: Combine these OpenXR requirements with our engine's own mandatory extensions (such as `VK_EXT_debug_utils`) and create the instance. -[IMPORTANT] -==== -**The String Parsing Pitfall**: `xrGetVulkanInstanceExtensionsKHR` returns the required extensions as a single, space-separated string (e.g., `"VK_KHR_external_memory_capabilities VK_KHR_get_physical_device_properties2"`). You must manually parse this string and split it into individual `const char*` entries before passing them to `vk::InstanceCreateInfo`. -==== - -The same principle applies to the physical device. The XR runtime may require specific device extensions, such as `VK_KHR_external_memory` or color space extensions, to safely share images between our application and the compositor process. - [source,cpp] ---- -// Example of merging requirements in our Engine's initialization +// Example of merging requirements in our Engine's initialization using designated initializers std::vector instanceExtensions = engineDefaults.getInstanceExtensions(); // 1. Query the length of the extension string @@ -57,25 +56,44 @@ xrGetVulkanInstanceExtensionsKHR(xrInstance, systemId, xrExtensionCount, &xrExte std::stringstream ss(xrExtensionString); std::string extension; while (ss >> extension) { - // Check if we already have it, then add it if (std::find(instanceExtensions.begin(), instanceExtensions.end(), extension) == instanceExtensions.end()) { instanceExtensions.push_back(strdup(extension.c_str())); } } -vk::InstanceCreateInfo createInfo({}, &applicationInfo, - static_cast(layers.size()), layers.data(), - static_cast(instanceExtensions.size()), instanceExtensions.data()); +vk::ApplicationInfo applicationInfo{ + .pApplicationName = "SpatialEngine", + .applicationVersion = 1, + .pEngineName = "VulkanSpatial", + .engineVersion = 1, + .apiVersion = VK_API_VERSION_1_3 +}; + +vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &applicationInfo, + .enabledLayerCount = static_cast(layers.size()), + .ppEnabledLayerNames = layers.data(), + .enabledExtensionCount = static_cast(instanceExtensions.size()), + .ppEnabledExtensionNames = instanceExtensions.data() +}; vk::raii::Instance instance(context, createInfo); ---- -== Why the Handshake Matters: Zero-Copy Efficiency +[IMPORTANT] +==== +**The String Parsing Pitfall**: `xrGetVulkanInstanceExtensionsKHR` returns the required extensions as a single, space-separated string (e.g., `"VK_KHR_external_memory_capabilities VK_KHR_get_physical_device_properties2"`). You must manually parse this string and split it into individual `const char*` entries before passing them to `vk::InstanceCreateInfo`. +==== + +== Zero-Copy Efficiency: Advanced Vulkan Advantages The ultimate goal of this handshake is **Zero-Copy Efficiency**. If we selected a GPU that the XR runtime couldn't talk to, the operating system would be forced to copy our final rendered frames through system memory to reach the headset's display. This would destroy our frame budget and introduce "judder"—the visual stuttering that causes motion sickness in XR. -By following the `XR_KHR_vulkan_enable2` protocol, we guarantee that our frames stay on the GPU at all times, moving from our render pipeline to the headset's display with the absolute minimum possible latency. +While the runtime provides mandatory extensions for the graphics handshake, Vulkan allows us to go further: + +* **Enabling Custom Profiling Extensions**: The handshake doesn't automatically enable engine-specific tools like `VK_EXT_calibrated_timestamps`. Vulkan allows you to merge these custom requirements into the XR-driven initialization, providing deep performance insights that the OpenXR standard does not natively expose. +* **Intercepting API Calls for Debugging**: By using Vulkan's layer system, you can intercept the internal commands the runtime sends to your GPU, allowing you to debug synchronization issues or resource leaks that occur within the OpenXR-Vulkan bridge. -In the next section, we will look at **Hardware Alignment**, where we ensure that the `VkPhysicalDevice` we select is the exact same one the headset is physically connected to. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_KHR_vulkan_enable2[XR_KHR_vulkan_enable2 Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc index 360eed15f..eee1c87a7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/03_hardware_alignment_luid.adoc @@ -3,26 +3,29 @@ In many modern computing environments, especially high-end gaming desktops, it is common to have multiple GPUs—such as an integrated GPU on the processor and a dedicated high-performance card. For spatial computing, it is absolutely critical that both our application and the OpenXR runtime are using the exact same physical hardware. -If our engine renders a frame on GPU A, but the headset is physically connected to GPU B, the operating system would be forced to copy that image across the **PCIe** (Peripheral Component Interconnect Express) bus between the two cards. This "cross-talk" introduces massive latency and can easily break the tight timing requirements needed for a comfortable XR experience. To prevent this, we use the **Locally Unique Identifier (LUID)**. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Aligning your Vulkan physical device with the XR runtime's preferred hardware is a mandatory step to ensure high-performance resource sharing. Using the LUID (Locally Unique Identifier) ensures that both systems are talking to the same physical silicon. == The "PCIe Tax": Why Alignment is Mandatory To understand why we care about the LUID, we must consider the cost of data movement. The PCIe bus is the highway between your CPU, system RAM, and your GPUs. While it is very fast, it is still orders of magnitude slower than the internal memory bus of a modern GPU (VRAM). -* **Internal VRAM Copy**: Moving an image within the same GPU is almost instantaneous, often happening at speeds of 500 GB/s to 1 TB/s. -* **Cross-GPU Copy**: Moving an image from GPU A to GPU B requires sending the data over the PCIe bus to system RAM, and then back down to GPU B. This adds several milliseconds of latency. - -In XR, every millisecond counts. If your alignment is wrong, you might lose 3-5ms just moving the pixels to the display. That is nearly half of your entire frame budget for a 90Hz or 120Hz headset! +* **Display Discovery**: The runtime has negotiated with the operating system (e.g., via DXGI on Windows) to identify which physical GPU is electrically connected to the headset's display. +* **Avoiding Cross-GPU Copies**: If our engine renders a frame on GPU A, but the headset is connected to GPU B, the OS would be forced to copy that image across the **PCIe** bus. This adds several milliseconds of latency—potentially half of your frame budget for a 90Hz headset. +* **Internal VRAM Efficiency**: Moving an image within the same GPU is almost instantaneous. By pinning the application to the same GPU, the runtime ensures your frames stay within high-speed VRAM at all times. == What is an LUID? -An **LUID** is a 64-bit value guaranteed to be unique on the local machine until the next reboot. Unlike a **UUID** (Universally Unique Identifier), which is persistent across machines and reboots, the LUID is a transient hardware handle provided by the operating system (specifically Windows via **DXGI**, the DirectX Graphics Infrastructure, though Vulkan provides a cross-platform way to access it). +An **LUID** (Locally Unique Identifier) is a 64-bit value guaranteed to be unique on the local machine until the next reboot. Unlike a **UUID**, which is persistent across machines, the LUID is a transient hardware handle provided by the operating system. In the context of the OpenXR-Vulkan handshake, the LUID serves as the hardware "fingerprint" of the GPU. OpenXR tells us: "I am currently talking to the GPU with this specific LUID," and we must search through our available `VkPhysicalDevice` handles until we find the one that matches. == Querying the XR Device -Once we have initialized our `xr::Instance`, we can query the specific `VkPhysicalDevice` handle that the runtime expects us to use. We do this by calling `xrGetVulkanGraphicsDevice2KHR`. +Once we have initialized our `xr::Instance`, we can query the specific hardware identity that the runtime expects us to use. We do this by calling `xrGetVulkanGraphicsDevice2KHR`. [source,cpp] ---- @@ -30,15 +33,15 @@ VkPhysicalDevice xrRequiredDevice; xrGetVulkanGraphicsDevice2KHR(xrInstance, systemId, *instance, &xrRequiredDevice); ---- -While we can get the raw handle directly from the runtime, it is often safer and more robust to match based on the LUID, especially when our engine's architecture abstracts physical device selection or when multiple Vulkan instances are present. Matching via LUID ensures that we are selecting the device based on hardware identity rather than just a transient API handle. +While we can get the raw handle directly from the runtime, matching via LUID is often more robust, especially when our engine's architecture abstracts physical device selection or when multiple Vulkan instances are present. == Matching the LUID in Vulkan -To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDProperties` structure. This structure contains the `deviceLUID` and a boolean `deviceLUIDValid`. In the `vulkan-hpp` RAII world, we can retrieve this by chaining structures in a `getProperties2` call. +To find the LUID of a Vulkan physical device, we query the `VkPhysicalDeviceIDProperties` structure. In the `vulkan-hpp` RAII world, we can retrieve this by chaining structures in a `getProperties2` call. [source,cpp] ---- -// Iterating through physical devices to find a match +// Iterating through physical devices to find a match using vulkan-hpp RAII for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { auto props2 = physicalDevice.getProperties2(); const auto& idProps = props2.get(); @@ -54,10 +57,17 @@ for (const auto& physicalDevice : instance.enumeratePhysicalDevices()) { == Cross-Process Memory Visibility -The LUID isn't just for selection; it is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our game engine, the images we render must be shared across process boundaries. +The LUID isn't just for selection; it is the foundation of **Cross-Process Memory Visibility**. Because the XR runtime usually lives in a separate process from our engine, the images we render must be shared across process boundaries. + +By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies. + +== Advanced: Multi-GPU Load Balancing and Device Groups + +While an OpenXR session is typically tied to the single GPU connected to the display, Vulkan allows us to go further: -Vulkan 1.3 makes this easier by standardizing external memory handles, but these handles only work if the memory was allocated on the same physical silicon. By aligning our hardware selection via the LUID, we guarantee that the "Wait-Acquire-Release" cycle can happen entirely on-device, without expensive CPU-side synchronization or system memory copies. +* **Vulkan Device Groups**: You can use `VK_KHR_device_group` to treat multiple GPUs as a single logical device. This allows you to use a secondary GPU for heavy compute tasks (like physics or scene decomposition) and aggregate the results on the primary GPU before handing the final frame to the XR compositor. +* **Handling Hardware Changes**: If the hardware configuration changes (e.g., an external GPU is disconnected), you can use Vulkan's hardware abstraction to detect these changes and prompt the user for a clean engine restart, ensuring that the hardware LUID handshake remains valid. -In the final part of our handshake, we will ensure that our selected device is configured with the mandatory **Vulkan 1.3 features** required for a modern spatial pipeline. +TIP: For more information on hardware alignment, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VkPhysicalDeviceIDProperties[Vulkan Specification on Device IDs], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/02_system_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc index 40a0a8109..1ebd1ff6c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/04_vulkan_1_3_feature_requirements.adoc @@ -3,64 +3,72 @@ The final step in our spatial handshake is ensuring that our `VkDevice` is initialized with the correct set of features. While OpenXR can technically run on older Vulkan versions, the **Vulkan 1.3** specification is the "gold standard" for modern spatial computing. It codifies several previously optional extensions into the core API, providing a guaranteed baseline of high-performance tools. -TIP: For more information on Vulkan 1.3 and core features, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. -In this section, we'll look at the three mandatory "pillars" of Vulkan 1.3 that our spatial pipeline depends on. +Enabling modern Vulkan features like Timeline Semaphores and Dynamic Rendering is essential for building a low-latency spatial pipeline that adheres to OpenXR best practices. == 1. Timeline Semaphores: The Async Heartbeat In a standard application, we use binary semaphores to sync the GPU. A binary semaphore is like a single-use "go" signal. In XR, where we have complex dependencies between the CPU simulation, the GPU render, and the XR compositor (which might be in a different process), binary semaphores are too rigid. -**Timeline Semaphores** provide a 64-bit monotonically increasing value. Think of them as a "Global Clock" for your GPU work. This allows us to express complex "happens-before" relationships: +**Timeline Semaphores** provide a 64-bit monotonically increasing value. Think of them as a "Global Clock" for your GPU work. The XR runtime uses these to signal exactly when a frame is ready for composition and to manage the "Global Clock" of the GPU work, ensuring that reprojection happens at the last possible moment. * **Pacing**: We can signal that "Frame 500 is ready for composition" by simply reaching value `500` on a single timeline. * **Late Latching**: We can tell the GPU to "Wait until value 501 is signaled by the CPU" before starting the final matrix update. -* **Cross-Process**: Because they can be exported to OS handles, timeline semaphores allow our engine and the XR runtime to stay perfectly in sync without expensive CPU read-backs. +* **Cross-Process Synchronization**: Because they can be exported to OS handles, timeline semaphores allow our engine and the XR runtime to stay perfectly in sync without expensive CPU read-backs. == 2. Synchronization 2: Mastering Queue Ownership -The original Vulkan synchronization API was notoriously verbose and error-prone. **Synchronization 2** (`VK_KHR_synchronization2`) simplifies this by combining pipeline stages and access masks into a more unified structure (`VkDependencyInfo`). +**Synchronization 2** (`VK_KHR_synchronization2`) simplifies synchronization by combining pipeline stages and access masks into a more unified structure. In XR, this is critical for handling **Queue Ownership Transfers**. -In XR, this is critical for handling **Queue Ownership Transfers**. Because the XR runtime owns the swapchain images, we must perform a "Release" operation on our queue when we're done rendering, and the runtime performs an "Acquire" on its side. Synchronization 2 makes these hand-offs explicit and readable, significantly reducing the risk of "Race Conditions" between our engine and the headset compositor. +Because the XR runtime owns the swapchain images, we must perform a "Release" operation on our queue when we're done rendering, and the runtime performs an "Acquire" on its side. Using Synchronization 2, the runtime performs these high-performance hand-offs explicitly, ensuring that the engine and compositor never access the same memory simultaneously. == 3. Dynamic Rendering: Ultimate Flexibility -If you've followed our engine-building journey, you're already intimately familiar with **Dynamic Rendering**. In the context of spatial computing, this isn't just a convenient refactoring—it's an absolute necessity. +By using `vkCmdBeginRendering` directly on our XR swapchain images, we avoid the heavy overhead and rigid state of legacy `VkRenderPass` objects. * **Asymmetric Views**: Most headsets use "Canted Displays" where the left and right eye frustums are not parallel. This often requires different viewport and scissor settings for each eye. * **Dynamic Resolution**: To maintain a steady 90Hz, the engine might need to drop the resolution of peripheral views instantly. -* **No Rigid State**: By using `vkCmdBeginRendering` directly on our XR swapchain images, we avoid the heavy overhead and rigid state of legacy `VkRenderPass` and `VkFramebuffer` objects. +* **Runtime Viewport Management**: The runtime relies on the flexibility of **Dynamic Rendering** to handle varied headset architectures without needing to re-create expensive framebuffer state for every eye. == Enabling Vulkan 1.3 Features in RAII -Vulkan 1.3 simplifies feature enablement. Instead of deep, nested `pNext` chains, we can use the unified `VkPhysicalDeviceVulkan13Features` (and its predecessors for 1.3 and 1.2) to toggle the core requirements. +Vulkan 1.3 simplifies feature enablement. Instead of deep, nested `pNext` chains, we can use the unified `VkPhysicalDeviceVulkan13Features` (and its predecessors) to toggle the core requirements using designated initializers. [source,cpp] ---- -// Enabling mandatory features for our spatial pipeline, including new Vulkan 1.3 capabilities -vk::PhysicalDeviceVulkan13Features features13; -features13.dynamicRendering = VK_TRUE; -features13.synchronization2 = VK_TRUE; +// Enabling mandatory features for our spatial pipeline using designated initializers +vk::PhysicalDeviceVulkan12Features features12{ + .timelineSemaphore = VK_TRUE +}; + +vk::PhysicalDeviceVulkan13Features features13{ + .pNext = &features12, + .synchronization2 = VK_TRUE, + .dynamicRendering = VK_TRUE +}; + +vk::DeviceCreateInfo createInfo{ + .pNext = &features13, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data() +}; -vk::PhysicalDeviceVulkan12Features features12; -features12.timelineSemaphore = VK_TRUE; +vk::raii::Device device(physicalDevice, createInfo); +---- -// Chain them together -features13.pNext = &features12; +== Advanced: Beyond the OpenXR Mandatory Set -vk::DeviceCreateInfo createInfo({}, - static_cast(queueCreateInfos.size()), queueCreateInfos.data(), - 0, nullptr, // Layers (deprecated) - static_cast(deviceExtensions.size()), deviceExtensions.data(), - nullptr); // pEnabledFeatures (use pNext instead) +While the handshake ensures a baseline, Vulkan 1.3 allows you to enable "Next-Gen" features that the OpenXR specification does not yet mandate: -// Ensure we are explicitly asking for Vulkan 1.3 core features if needed -// Note: In Vulkan 1.3, many of these are enabled by default if the version is supported, -// but explicit enablement is still best practice for engine portability. -createInfo.pNext = &features13; +* **Mesh Shading and Ray Tracing**: You can use Vulkan's feature discovery to enable these powerful pipelines for high-fidelity spatial effects, such as real-time spatial reflections or complex geometry processing, without waiting for the OpenXR standard to catch up. +* **Custom Pipeline Caches**: By managing your own **VkPipelineCache**, you can ensure that your spatial shaders load instantly across different headset configurations, bypassing the runtime's generic shader management. -vk::raii::Device device(physicalDevice, createInfo); ----- +TIP: For more information on Vulkan 1.3 and core features, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. == Summary of the Handshake diff --git a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc index 670607455..a455fa7e7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/02_OpenXR_Vulkan_Handshake/05_incorporating_into_the_engine.adoc @@ -156,15 +156,18 @@ In `Renderer::createLogicalDevice`, we must explicitly enable the Vulkan 1.3 fea // renderer_core.cpp bool Renderer::createLogicalDevice(bool enableValidationLayers) { // ... - // Enable Vulkan 1.3 features - vk::PhysicalDeviceVulkan13Features vulkan13Features; - vulkan13Features.dynamicRendering = vk::True; - vulkan13Features.synchronization2 = vk::True; - - // Enable Multiview (if in XR mode) - vk::PhysicalDeviceMultiviewFeatures multiviewFeatures; - multiviewFeatures.multiview = xrMode ? vk::True : vk::False; - multiviewFeatures.pNext = &vulkan13Features; + + // Enable Vulkan 1.3 features using designated initializers + vk::PhysicalDeviceVulkan13Features vulkan13Features{ + .synchronization2 = vk::True, + .dynamicRendering = vk::True + }; + + // Enable Multiview (if in XR mode) using designated initializers + vk::PhysicalDeviceMultiviewFeatures multiviewFeatures{ + .pNext = &vulkan13Features, + .multiview = xrMode ? vk::True : vk::False + }; // ... create vk::raii::Device with multiviewFeatures in pNext ... } diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc index d1eb70450..628b8b55e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc @@ -1,17 +1,21 @@ :pp: {plus}{plus} = External Image Negotiation -In a standard Vulkan application, we use `vkCreateSwapchainKHR` to create a surface-bound swapchain. In OpenXR, we replace this entire mechanism with the `XrSwapchain`. Instead of the operating system's windowing system (like Win32 or X11) managing the buffers, the **XR Runtime** acts as our swapchain provider. +In a standard Vulkan application, we use `vkCreateSwapchainKHR` to create a surface-bound swapchain. In OpenXR, we replace this entire mechanism with the `XrSwapchain`. Instead of the operating system's windowing system managing the buffers, the **XR Runtime** acts as our swapchain provider. -This negotiation starts by asking the runtime what image formats it supports and choosing the one that best fits our engine's internal pipeline. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Inverting the ownership of the swapchain is a fundamental architectural change required by OpenXR. You must negotiate with the runtime to retrieve the images that your engine will render into. == The Inversion of Control: The Swapchain Provider -To understand this negotiation, you must shift your mindset. In a desktop app, *you* create the swapchain and the OS *consumes* it. In OpenXR, the **Runtime creates the images** and *lends* them to you. +To understand this negotiation, you must shift your mindset. In a desktop app, *you* create the swapchain and the OS *consumes* it. In OpenXR, the **Runtime acts as the swapchain provider**, creating the images and "lending" them to you. -1. **Ownership**: The runtime allocates the VRAM and manages the memory layout. -2. **Compatibility**: The runtime knows exactly which bit-depths and color spaces its internal compositor can handle without performing expensive conversions. -3. **Efficiency**: By providing the images directly, the runtime can ensure they are allocated in a memory region that is visible to both your engine and the compositor process. +* **VRAM Allocation**: The runtime allocates the VRAM and manages the memory layout, ensuring the images are allocated in a memory region visible to both your engine and the compositor process. +* **Memory Tiling and Alignment**: The runtime chooses the optimal tiling and alignment for its internal compositor, ensuring that it can read your frames with maximum bandwidth. +* **Efficiency**: By providing the images directly, the runtime can perform **Zero-Copy** hand-offs. When you finish rendering, the runtime takes that exact same memory and uses it for the final compositor pass. == Enumerating Swapchain Formats @@ -27,14 +31,14 @@ xrEnumerateSwapchainFormats(xrSession, formatCount, &formatCount, formats.data() It is important to note that OpenXR returns these as `int64_t` values, which we must cast to `VkFormat` (or `vk::Format` in our RAII engine). Usually, we want a high-dynamic-range (**HDR**) format like `VK_FORMAT_R16G16B16A16_SFLOAT` or a standard **SRGB** format like `VK_FORMAT_R8G8B8A8_SRGB`. -[TIP] -==== -**Linear vs SRGB**: If your engine performs its own gamma correction in a post-process pass, you should prefer a linear format. However, if you are outputting directly to the headset, using an `_SRGB` format allows the hardware to perform the conversion for free during the final write. -==== +**Advanced: Handling Format and Usage Restrictions**: +While OpenXR limits the available swapchain formats to a specific enumerated list, Vulkan allows you to go further: +* **Custom Internal Formats**: You can render to a custom internal format (such as a highly optimized compressed format) and perform a final **Image Copy** or **Blit** to the OpenXR-supported swapchain at the last moment. +* **Storage Image Usage**: Some runtimes may not support `VK_IMAGE_USAGE_STORAGE_BIT` for their swapchain images. In these cases, you can use Vulkan to create a separate storage image, perform your compute work, and copy the results into the XR swapchain. == Creating the XrSwapchain -Once we have chosen a format, we fill out an `XrSwapchainCreateInfo` structure. This structure is similar to `VkSwapchainCreateInfoKHR`, but it also includes XR-specific fields like **sampleCount** (for multisampling) and **faceCount** (for cubemap views). +Once we have chosen a format, we fill out an `XrSwapchainCreateInfo` structure using designated initializers. [source,cpp] ---- @@ -69,8 +73,10 @@ Each `XrSwapchainImageVulkanKHR` contains a `VkImage` handle. It is these handle == Why "Zero-Copy" Matters: The Compositor Pipeline -By negotiating the images this way, we ensure a **Zero-Copy** hand-off. When we finish rendering, we don't copy our frame to a separate buffer. Instead, we simply signal to the runtime that we are done with its image. The runtime then takes that exact same memory and uses it for the final compositor pass. +By negotiating the images this way, we ensure a **Zero-Copy** hand-off. When we finish rendering, we don't copy our frame to a separate buffer. Instead, we simply signal to the runtime that we are done with its image. The runtime then takes that exact same memory and uses it for the final compositor pass (distortion correction, chromatic aberration, etc.). This efficiency is the difference between a high-performance 90Hz experience and a laggy, uncomfortable one. In the next section, we'll see how to wrap these raw Vulkan handles into the engine's `vk::raii` abstractions. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#xrEnumerateSwapchainFormats[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. + xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc index ecdf2610c..6902681e9 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc @@ -3,26 +3,28 @@ Once we have our raw `VkImage` handles from OpenXR, we need to integrate them into our engine's Resource Acquisition Is Initialization (**RAII**) framework. Our engine uses the `vk::raii` namespace, which provides high-level C++ wrappers that automatically manage the lifetime of Vulkan objects. -However, there is a catch: `vk::raii` objects typically assume they own the resource and will attempt to destroy it when they go out of scope. Because the XR runtime owns these swapchain images, we must ensure that our engine does not accidentally call `vkDestroyImage` on them. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Integrating external runtime-owned resources into a modern RAII framework requires careful handling of ownership and destruction contracts. This is a common pattern when interfacing Vulkan with external systems. == The Ownership Contract: "Lent, Not Given" -When we receive a `VkImage` from `xrEnumerateSwapchainImages`, we are entering into a legal contract with the XR runtime. +When we receive a `VkImage` from OpenXR, we are entering into a legal contract with the XR runtime. The runtime provides the raw image because it doesn't know your engine's specific needs—for example, if you are using **Multiview**, you might need an `e2DArray` view, whereas a simple engine might just want a standard `e2D` view. -* **We can**: Bind the image as a color attachment, transition its layout, and write to it with our shaders. +* **We can**: Bind the image as a color attachment, transition its layout, and create our own views. * **We cannot**: Destroy the image, change its allocation flags, or attempt to free its memory. - -If our engine attempts to call `vkDestroyImage` (which a standard `vk::raii::Image` would do in its destructor), we will likely crash the entire XR system—including the headset runtime and possibly our own graphics drivers. +* **Reference Counting**: The runtime keeps an internal reference count for each image, waiting for you to signal completion via the frame lifecycle. +* **Memory Residency**: The runtime ensures the memory stays resident on the GPU, handling any necessary OS-level "pinning." == Non-Owning Image Wrappers -The simplest way to handle this in `vulkan-hpp` is to treat the `VkImage` handles as non-owning `vk::Image` objects. While we lose the automatic cleanup of the image itself, we still gain the benefit of the type-safe C++ API. - -We then create our own `vk::raii::ImageView` for each image. Unlike the image itself, the **Image View** is something our application creates and owns, so it can (and should) be managed by our RAII system. +The simplest way to handle this in `vulkan-hpp` is to treat the `VkImage` handles as non-owning `vk::Image` objects and create our own `vk::raii::ImageView` for each one using designated initializers. [source,cpp] ---- -// Wrapping the raw handles into our engine's per-frame data +// Wrapping the raw handles into our engine's per-frame data using designated initializers struct XrFrameBuffer { vk::Image image; // Non-owning handle vk::raii::ImageView imageView = nullptr; // RAII owned by us @@ -33,9 +35,16 @@ for (const auto& xrImg : xrImages) { XrFrameBuffer fb; fb.image = vk::Image(xrImg.image); - // Note: If using multiview, the view type might be e2DArray instead of e2D - vk::ImageViewCreateInfo viewInfo({}, fb.image, vk::ImageViewType::e2D, - format, {}, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + vk::ImageViewCreateInfo viewInfo{ + .image = fb.image, + .viewType = vk::ImageViewType::e2D, // or e2DArray for multiview + .format = chosenFormat, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .levelCount = 1, + .layerCount = 1 + } + }; fb.imageView = vk::raii::ImageView(device, viewInfo); framebuffers.push_back(std::move(fb)); @@ -44,21 +53,36 @@ for (const auto& xrImg : xrImages) { == Integrating with Dynamic Rendering -Because we are using **Dynamic Rendering** (introduced as an extension and made core in Vulkan 1.3), we don't need to wrap these images into `VkFramebuffer` objects. Instead, we can directly reference the `vk::raii::ImageView` in our `vk::RenderingAttachmentInfo`. +Because we are using **Dynamic Rendering** (introduced in Vulkan 1.3), we don't need to wrap these images into legacy `VkFramebuffer` objects. Instead, we can directly reference the `vk::raii::ImageView` in our `vk::RenderingAttachmentInfo`. [source,cpp] ---- -vk::RenderingAttachmentInfo colorAttachment{}; -colorAttachment.imageView = *framebuffers[imageIndex].imageView; -colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; -colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; -colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; -colorAttachment.clearValue = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); - -vk::RenderingInfo renderingInfo({}, renderRect, 1, 0, 1, &colorAttachment); +// Rendering directly to the XR image using Dynamic Rendering and designated initializers +vk::RenderingAttachmentInfo colorAttachment{ + .imageView = *framebuffers[imageIndex].imageView, + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f) +}; + +vk::RenderingInfo renderingInfo{ + .renderArea = renderRect, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment +}; + commandBuffer.beginRendering(renderingInfo); ---- +== Advanced: Interoperability with External APIs + +While OpenXR handles the image handles, Vulkan's **External Memory** and **Foreign Queue** extensions allow you to go beyond the spatial standard: + +* **Cross-API Sharing**: You can share these swapchain images with other APIs, such as **CUDA** for high-speed vision processing or a hardware video decoder for 360-degree playback. +* **Custom Compositing**: This level of interoperability allows your engine to perform complex, non-spatial processing on the exact same physical memory being used by the XR runtime. + == Handling the Lifetime: Hybrid Destruction In our engine architecture, we keep these `XrFrameBuffer` objects alive for the duration of the `XrSwapchain`'s existence. When the user closes the application or switches environments, we destroy the `XrSwapchain` via `xrDestroySwapchain`. @@ -68,8 +92,6 @@ This leads to a **Hybrid Destruction** flow: 2. **RAII Destruction**: Our `XrFrameBuffer` structures go out of scope, causing their `vk::raii::ImageView` members to automatically call `vkDestroyImageView`. 3. **Safety**: The non-owning `vk::Image` handles disappear without any Vulkan call, fulfilling our contract with the runtime. -This hybrid approach—using raw handles for external resources and RAII for internal ones—is a common pattern when interfacing Vulkan with external runtimes like OpenXR, CUDA, or video decoders. - -In the next section, we will look at the **Memory Ownership Lifecycle**, where we master the delicate dance of waiting, acquiring, and releasing these images during our frame loop. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#swapchain_images[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/02_external_image_negotiation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc index 89e712fba..71c07e629 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/04_memory_ownership_lifecycle.adoc @@ -3,39 +3,47 @@ In a standard desktop Vulkan application, we use `vkAcquireNextImageKHR` to get an image index from the swapchain. When we are done, we call `vkQueuePresentKHR`. In OpenXR, this is replaced by a more explicit three-step lifecycle: **Wait**, **Acquire**, and **Release**. -This lifecycle is designed to allow the XR runtime to manage high-frequency tasks like asynchronous reprojection while keeping our engine's rendering pipeline efficient. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +The Wait-Acquire-Release cycle is the core synchronization mechanism for OpenXR frame processing. Implementing this correctly is mandatory for any high-performance spatial engine. == The Three-Step Dance: Syncing with the Compositor -Unlike a desktop window, which can handle images whenever the GPU is ready, an XR headset is a rigid, time-sensitive display. The compositor needs to know exactly when it can "take" an image to show it to the user. +Unlike a desktop window, which can handle images whenever the GPU is ready, an XR headset is a rigid, time-sensitive display. The compositor manages high-frequency tasks like **Asynchronous Reprojection** while keeping our engine's rendering pipeline efficient through a three-step lifecycle: 1. **Wait**: "Is there an image available for me to start drawing into?" 2. **Acquire**: "Give me the index of that image and lock it for my use." 3. **Release**: "I'm done drawing; you can have it back now." -This dance ensures that the engine and the compositor never attempt to access the same memory at the same time, preventing the "tearing" and "stuttering" that can occur if synchronization fails. +This dance ensures that the engine and the compositor never attempt to access the same memory at the same time, preventing "tearing" or "stuttering." == 1. Wait (`xrWaitSwapchainImage`) -The first step is `xrWaitSwapchainImage`. This call is a blocking operation (with an optional timeout) that waits until the XR runtime is ready for the application to begin writing to the next image in the swapchain. +The first step is `xrWaitSwapchainImage`. This call is a blocking operation that waits until the XR runtime is ready for the application to begin writing to the next image in the swapchain. [source,cpp] ---- -XrSwapchainImageWaitInfo waitInfo{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; -waitInfo.timeout = XR_INFINITE_DURATION; +XrSwapchainImageWaitInfo waitInfo{ + .type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, + .timeout = XR_INFINITE_DURATION +}; xrWaitSwapchainImage(xrSwapchain, &waitInfo); ---- -**Why wait?** If our engine is rendering faster than the headset's refresh rate (e.g., we are rendering at 144 FPS but the headset is 90Hz), `xrWaitSwapchainImage` will block our engine thread. This acts as the "Pacing" mechanism for our entire engine loop, ensuring we don't waste GPU power on frames that will never be shown. +**Pacing and Exhaustion**: If your engine is rendering faster than the headset's refresh rate, `xrWaitSwapchainImage` will block your engine thread, acting as a natural pacing mechanism. However, if you don't release images in a timely manner, this call will block indefinitely, causing your entire engine to stall. == 2. Acquire (`xrAcquireSwapchainImage`) -Once the wait is satisfied, we call `xrAcquireSwapchainImage`. This function actually identifies which specific image index in our swapchain we should render into. +Once the wait is satisfied, we call `xrAcquireSwapchainImage`. This function identifies which specific image index in our swapchain we should render into. [source,cpp] ---- uint32_t imageIndex; -XrSwapchainImageAcquireInfo acquireInfo{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; +XrSwapchainImageAcquireInfo acquireInfo{ + .type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO +}; xrAcquireSwapchainImage(xrSwapchain, &acquireInfo, &imageIndex); ---- @@ -43,45 +51,55 @@ After this call, our application formally "owns" the image at `imageIndex`. This == 3. Synchronization and Layout Transitions -Even though OpenXR manages the ownership, we are still responsible for Vulkan-side synchronization. When an image is returned to us, it might be in a state used by the XR compositor (usually `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` or `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`). - -We must use a **Pipeline Barrier** (ideally via **Synchronization 2** in Vulkan 1.3) to transition it to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` before we can use it in our dynamic rendering pass. +Even though OpenXR manages the ownership, we are still responsible for Vulkan-side synchronization. We use **Synchronization 2** (Vulkan 1.3) to transition the image to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` before use. [source,cpp] ---- -vk::ImageMemoryBarrier2 barrier{}; -barrier.oldLayout = vk::ImageLayout::eUndefined; // Safe to use undefined if we don't care about previous contents -barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; -barrier.image = framebuffers[imageIndex].image; -barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; -barrier.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; -barrier.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput; -barrier.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite; - -vk::DependencyInfo depInfo({}, 0, nullptr, 0, nullptr, 1, &barrier); +vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = {}, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .image = framebuffers[imageIndex].image, + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } +}; + +vk::DependencyInfo depInfo{ + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier +}; commandBuffer.pipelineBarrier2(depInfo); ---- == 4. Release (`xrReleaseSwapchainImage`) -Finally, once our command buffer has been submitted to the GPU (but not necessarily finished executing), we call `xrReleaseSwapchainImage`. This tells the XR runtime: "I am finished writing to this image; you may take it back for composition." +Finally, once our command buffer has been submitted to the GPU, we call `xrReleaseSwapchainImage`. This tells the XR runtime: "I am finished writing to this image; you may take it back for composition." [source,cpp] ---- -XrSwapchainImageReleaseInfo releaseInfo{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; +XrSwapchainImageReleaseInfo releaseInfo{ + .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO +}; xrReleaseSwapchainImage(xrSwapchain, &releaseInfo); ---- -**Crucial Detail**: Calling `Release` does not mean the GPU has finished rendering. It means the *CPU* is done recording commands. The runtime will use the synchronization primitives we enabled during the handshake (like timeline semaphores) to wait for the GPU to actually finish the work before it displays the image. +**Asynchronous Reprojection**: After you `Release` the image, the runtime takes it into its own high-priority queue. If your engine misses its next frame deadline, the runtime will take the *last* released image and "warp" it based on the user's latest head movement, maintaining comfort even during performance dips. -== Summary of Resource Management +== Advanced: Coordinating Asynchronous Compute -We have now seen how to: +While OpenXR manages the acquisition cycle, Vulkan's **Timeline Semaphores** allow you to create a granular synchronization relationship with your own asynchronous compute or physics queues. -1. Negotiate image formats and sizes with the XR runtime. -2. Wrap raw runtime-owned images into our RAII engine structures. -3. Manage the lifecycle of image ownership during our frame loop. +* **Granular Release**: You can ensure that an image is only released after both the render pass and a secondary compute pass (such as real-time audio synthesis based on the frame data) are complete. +* **Latency Monitoring**: You can use Vulkan **Query Pools** to monitor your own GPU timing and ensure you are releasing resources fast enough to avoid stalling the engine. -With our resources properly managed, we can now move on to the actual rendering process. In the next chapter, we will see how to use **Dynamic Rendering** to render our spatial views directly into these images with maximum efficiency. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#xrWaitSwapchainImage[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/03_raii_resource_integration.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/03_Runtime_Owned_Swapchains/05_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc index ac51a811b..f8e51d07f 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc @@ -1,62 +1,76 @@ :pp: {plus}{plus} = Rendering into Spatial Swapchains -The beauty of the dynamic rendering system we've already built is that it doesn't care where an image comes from. Whether it's a standard swapchain or a set of images provided by an XR runtime, the process of beginning a rendering session remains consistent and lightweight. +The dynamic rendering system we've built is ideal for spatial computing because it doesn't care where an image comes from. Whether it's a standard swapchain or a set of images provided by an XR runtime, the process of beginning a rendering session remains consistent and lightweight. -== Binding XR Images: The Runtime Handover +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. -In our previous chapter, we learned how to wrap `XrSwapchainImageVulkanKHR` handles into `vk::raii::Image` and `vk::raii::ImageView`. With dynamic rendering, we take these image views and provide them directly to the `vk::RenderingInfo` structure at the start of each frame. +Rendering directly into runtime-owned swapchains using Vulkan's dynamic rendering is the recommended approach for modern spatial pipelines. It provides the maximum flexibility for the varied optical requirements of different headsets. -This represents a **Decoupling of the Graphics Pipeline**. Our engine no longer owns the final output buffer; it simply "paints" into whatever view the XR runtime provides. +== Binding XR Images: The Runtime Handover + +In our previous chapter, we learned how to wrap `XrSwapchainImageVulkanKHR` handles into `vk::raii::Image` and `vk::raii::ImageView`. With dynamic rendering, we provide these image views directly to the `vk::RenderingInfo` structure using designated initializers. [source,cpp] ---- -// Define the color attachment for our XR view -vk::RenderingAttachmentInfo colorAttachment{}; -colorAttachment.imageView = *xrImageView; // Our RAII-wrapped image view from the XR runtime -colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; -colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; -colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; -colorAttachment.clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.0f, 0.0f, 0.0f, 1.0f}}}; - -// Configure the rendering session -vk::RenderingInfo renderingInfo{}; -renderingInfo.renderArea = vk::Rect2D{{0, 0}, {xrWidth, xrHeight}}; -renderingInfo.layerCount = 1; // 1 for standard mono/stereo, more if using multiview arrays -renderingInfo.colorAttachmentCount = 1; -renderingInfo.pColorAttachments = &colorAttachment; - -// Begin dynamic rendering session +// 1. Define the color attachment using designated initializers +vk::RenderingAttachmentInfo colorAttachment{ + .imageView = *xrImageView, // Our RAII-wrapped view from the XR runtime + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = vk::ClearValue{.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}} +}; + +// 2. Configure the rendering session +vk::RenderingInfo renderingInfo{ + .renderArea = vk::Rect2D{{0, 0}, {xrWidth, xrHeight}}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment +}; + +// 3. Begin dynamic rendering session commandBuffer.beginRendering(renderingInfo); - -// ... Recording draw commands as usual ... - +// ... Recording draw commands ... commandBuffer.endRendering(); ---- -== Handling XR Pipeline State: The Power of `pNext` +By rendering directly into the runtime's images, we enable several critical features: +* **Zero-Copy Handover**: The runtime doesn't need to copy your pixels. Since you both share the same `VkImage` memory, the compositor simply points its own internal shaders at the memory you just filled. +* **Format Enforcement**: The runtime ensures efficient warping and blending without expensive format conversions. +* **Late-Stage Reprojection**: After you call `xrEndFrame`, the runtime uses the GPU's fixed-function hardware to stretch and skew your image based on the head's rotation at the very last microsecond. -As we've seen in our engine construction, our `vk::raii::Pipeline` doesn't require a static `vk::RenderPass` object. Instead, we use `vk::PipelineRenderingCreateInfo` in the `pNext` chain of our graphics pipeline. +== Handling XR Pipeline State: The Power of `pNext` -For XR, this is particularly powerful because we can quickly swap between different swapchain formats (like HDR10 or standard RGBA) simply by updating this info structure during pipeline creation. This allows our engine to support **Variable Quality Levels** or different headset requirements without recompiling shaders or rebuilding complex render pass objects. +We use `vk::PipelineRenderingCreateInfo` in the `pNext` chain of our graphics pipeline to ensure compatibility with the XR swapchain's format. [source,cpp] ---- -vk::PipelineRenderingCreateInfo pipelineRenderingInfo{}; -pipelineRenderingInfo.colorAttachmentCount = 1; -pipelineRenderingInfo.pColorAttachmentFormats = &xrColorFormat; -pipelineRenderingInfo.depthAttachmentFormat = vk::Format::eD32Sfloat; // If our XR view has depth - -// Link to our graphics pipeline create info -vk::GraphicsPipelineCreateInfo pipelineInfo{}; -pipelineInfo.pNext = &pipelineRenderingInfo; -// ... Standard pipeline configuration ... +vk::PipelineRenderingCreateInfo pipelineRenderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &xrColorFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat +}; + +vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &pipelineRenderingInfo, + // ... rest of pipeline configuration ... +}; ---- +For XR, this is particularly powerful because we can quickly swap between different formats or handle advanced features: +* **Handling MSAA Resolves**: If your engine requires MSAA, you must manage the multisampled buffers and the resolve step yourself. Vulkan's **Dynamic Rendering** allows you to perform this resolve by attaching both a multisampled transient image and the single-sampled XR swapchain image to the same rendering session. +* **Managing Depth Testing States**: While the runtime provides the color swapchain, you are responsible for creating and clearing your own depth/stencil buffers. Vulkan allows you to perfectly align these depth resources with the XR swapchain, ensuring stable spatial occlusion. + == Decoupling the Frame Loop: Separation of Concerns By using dynamic rendering, we've successfully decoupled our engine's internal rendering logic from the physical output. Our `VulkanContext` remains focused on managing pipeline state and recording commands, while the OpenXR loop manages the "render area" and provides the target images. This **Separation of Concerns** is what allows our engine to scale seamlessly. Whether we are rendering to a 4K desktop monitor or a dual-eye VR headset, the core command recording logic remains the same. The only thing that changes is *where* the image view points and *how* the projection matrices are calculated. +TIP: For more details on dynamic rendering, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#active-renderpass[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. + xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc index 3118d1bad..0d09b9402 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/03_stereo_viewport_scissor.adoc @@ -1,31 +1,39 @@ :pp: {plus}{plus} = Stereo Viewport & Scissor Management -When rendering for XR, we aren't just drawing to a single screen. We are drawing to multiple **Views**, each with its own perspective and projection. The most common case is **Stereo** (two views), but headsets with extreme fields of view or foveal insets can require many more. To manage this in a dynamic rendering context, we must master **Viewports** and **Scissors**. +When rendering for XR, we aren't just drawing to a single screen. We are drawing to multiple **Views**, each with its own perspective and projection. To manage this in a dynamic rendering context, we must master **Viewports** and **Scissors**. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Managing multiple viewports is a standard requirement for spatial views. You must use Vulkan's dynamic state to align your rendering with the specific optical parameters provided by the runtime for each eye. == The Architecture of the Eye: Asymmetric Projections -To understand why viewport and scissor management is so critical in XR, we must look at the human eye. Unlike standard 2D displays, where the viewer is typically centered and looking straight at the screen, XR lenses are often offset. +To understand why viewport and scissor management is so critical in XR, we must look at the lens optics. Unlike standard displays, XR lenses are often offset, leading to **Asymmetric Projections**. -1. **Optical Center Offset**: The center of the lens (where the image is clearest) might not be the center of the display panel. -2. **Field of View (FOV)**: Headsets often have larger outer FOVs than inner FOVs to provide a wider sense of immersion. -3. **Result**: This leads to **Asymmetric Projections**. The "frustum" is skewed. In our engine, this means we cannot simply render one eye and "flip" it for the other. Each eye must be treated as a unique camera with its own viewport and scissor constraints. +* **Lens Distortion Correction**: The runtime knows the physical curvature of the lenses. It provides a viewport that is typically *larger* than the physical display panel resolution (often 1.4x) to account for the detail lost during the "barrel distortion" warping process. +* **Optimal Pixel Density**: The runtime ensures that the "sweet spot" of the lens (the optical center) receives the highest concentration of rendered pixels. +* **Frustum Alignment**: The asymmetric FOV values are designed to perfectly align with the physical limits of the headset's housing, ensuring you don't waste GPU cycles rendering pixels physically obscured by the device. [source,cpp] ---- -// Configure the viewport for a specific eye view -vk::Viewport eyeViewport{}; -eyeViewport.x = 0.0f; -eyeViewport.y = 0.0f; -eyeViewport.width = static_cast(viewWidth); -eyeViewport.height = static_cast(viewHeight); -eyeViewport.minDepth = 0.0f; -eyeViewport.maxDepth = 1.0f; - -// Configure the scissor to match the viewport -vk::Rect2D eyeScissor{}; -eyeScissor.offset = vk::Offset2D{0, 0}; -eyeScissor.extent = vk::Extent2D{viewWidth, viewHeight}; +// Configure the viewport for a specific eye view using designated initializers +vk::Viewport eyeViewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(viewWidth), + .height = static_cast(viewHeight), + .minDepth = 0.0f, + .maxDepth = 1.0f +}; + +// Configure the scissor to match +vk::Rect2D eyeScissor{ + .offset = {0, 0}, + .extent = {viewWidth, viewHeight} +}; // Dynamic state update in our command buffer commandBuffer.setViewport(0, eyeViewport); @@ -39,7 +47,14 @@ Depending on the XR runtime, we might receive one large "atlas" texture containi * **Atlasing**: We use the `x` and `y` offsets of the `vk::Viewport` and `vk::Rect2D` to draw the eyes side-by-side. * **Array Textures**: We keep the viewport at `(0,0)` but we change the `imageView` layer in our `vk::RenderingAttachmentInfo`. -Dynamic rendering makes it easy to handle both cases by simply adjusting the attachment info. This flexibility allows our engine to support different headset architectures (like the Pimax wide-FOV headsets vs. the Meta Quest) without rewriting the render loop. +Dynamic rendering makes it easy to handle both cases by simply adjusting the attachment and viewport parameters. This flexibility allows our engine to support different headset architectures (like wide-FOV headsets vs. mobile VR) without rewriting the render loop. + +== Advanced: Variable Rate Shading and Stencil Masking + +Vulkan allows us to go beyond the standard rectangular viewport provided by OpenXR: + +* **Variable Rate Shading (VRS)**: You can use Vulkan's VRS to decouple the shading rate from the viewport resolution. This allows you to use a high-resolution viewport to satisfy lens distortion requirements while using a shading rate image to reduce the GPU load in the periphery. +* **Non-Rectangular Clipping**: OpenXR scissors are strictly rectangular. By using Vulkan **Stencil Buffers**, you can define "hidden area masks" that match the lens's visible aperture, ensuring you never render a single pixel that will be obscured by the headset's housing. == Optimal View Management: The Pacing Pattern @@ -50,8 +65,8 @@ To minimize state transitions and optimize performance, our engine follows a spe 3. **Apply Viewport/Scissor**: Tell the GPU the exact bounds of the current eye's frustum. 4. **Issue Draw Calls**: Render the scene using the predicted view and projection matrices. -By treating viewports and scissors as dynamic state, we avoid the heavy "Pipeline Stall" that would occur if we had to switch entire pipeline objects between eyes. This keeps our GPU utilization high and our latency low. +By treating viewports and scissors as dynamic state, we avoid the heavy "Pipeline Stall" that would occur if we had to switch entire pipeline objects between eyes. -With our rendering pipeline now fully adapted to the unique requirements of spatial views, we are ready to tackle the timing and pacing logic that makes XR truly immersive. In the next chapter, we will build our **Predictive Frame Loop**. +TIP: For more information on viewport and scissor state, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#pipelines-dynamic-state[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/02_rendering_to_spatial_swapchains.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/04_Dynamic_Rendering/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc index 37201efdd..bea8550b7 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc @@ -3,6 +3,12 @@ Transitioning our engine's main loop to the OpenXR lifecycle involves three primary functions: `xrWaitFrame`, `xrBeginFrame`, and `xrEndFrame`. These functions act as the "heartbeat" of our spatial application, replacing the standard unthrottled loop or legacy `glfwPollEvents` based pacing. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Synchronizing your engine's main loop with the OpenXR lifecycle is mandatory for photons-to-motion alignment. You must use the predicted display time provided by the runtime to ensure a smooth, stable experience. + == The Temporal Paradox: Rendering the Future To understand the XR lifecycle, you must first accept a fundamental truth: **You are always rendering a future that hasn't happened yet.** @@ -13,17 +19,19 @@ The XR lifecycle is designed to solve this by providing a **Predicted Display Ti == 1. `xrWaitFrame`: The Pacing Heartbeat -The `xrWaitFrame` function is our engine's synchronization point with the XR runtime. When we call this, the runtime may block our execution until it is the optimal time for us to begin processing the next frame. +The `xrWaitFrame` function is our engine's primary synchronization point. When we call this, the runtime may block our execution to keep us synced with the headset's refresh rate (e.g., 90Hz). [source,cpp] ---- -XrFrameWaitInfo frameWaitInfo{XR_TYPE_FRAME_WAIT_INFO}; +// Sync with the runtime heartbeat and retrieve the predicted display time XrFrameState frameState{XR_TYPE_FRAME_STATE}; +XrFrameWaitInfo frameWaitInfo{ + .type = XR_TYPE_FRAME_WAIT_INFO +}; xrWaitFrame(xrSession, &frameWaitInfo, &frameState); ---- -* **Throttling**: If your engine is too fast, `xrWaitFrame` will block to keep you synced with the headset's refresh rate (e.g., 90Hz). -* **The Prediction**: The `frameState` object returned by this call contains the `predictedDisplayTime`. This is the exact nanosecond when the pixels we are about to render will actually be shown on the headset's display. +* **Prediction**: The `frameState` contains the `predictedDisplayTime`. This is the exact nanosecond when the pixels we are about to render will actually be shown on the headset's display. * **Should We Render?**: `frameState.shouldRender` tells us if the runtime actually wants a frame. If the headset is off or the user is in a system menu, we might still "wait" but skip the "render" work to save power. == 2. `xrBeginFrame`: Opening the Window @@ -32,29 +40,38 @@ Once the wait is satisfied, we call `xrBeginFrame`. This signals the start of ou [source,cpp] ---- -XrFrameBeginInfo frameBeginInfo{XR_TYPE_FRAME_BEGIN_INFO}; +XrFrameBeginInfo frameBeginInfo{ + .type = XR_TYPE_FRAME_BEGIN_INFO +}; xrBeginFrame(xrSession, &frameBeginInfo); ---- -This is our engine's cue to start recording command buffers. It is important to keep the time between `xrWaitFrame` and `xrBeginFrame` as short as possible, as this is when we perform our simulation updates and view predictions. +It is important to keep the time between `xrWaitFrame` and `xrBeginFrame` as short as possible, as this is when we perform our simulation updates and view predictions. == 3. `xrEndFrame`: Submitting to the Compositor -The final step is `xrEndFrame`. This is the counterpart to `vkQueuePresentKHR`. +The final step is `xrEndFrame`, the spatial counterpart to `vkQueuePresentKHR`. [source,cpp] ---- -XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; -frameEndInfo.displayTime = frameState.predictedDisplayTime; -frameEndInfo.layerCount = 1; -frameEndInfo.pLayers = &layerPtr; +XrFrameEndInfo frameEndInfo{ + .type = XR_TYPE_FRAME_END_INFO, + .displayTime = frameState.predictedDisplayTime, + .environmentBlendMode = chosenBlendMode, + .layerCount = 1, + .layers = &layerPtr +}; xrEndFrame(xrSession, &frameEndInfo); ---- -Instead of just "presenting" a single image, `xrEndFrame` takes an array of **Composition Layers**. +Instead of just "presenting" a single image, `xrEndFrame` takes an array of **Composition Layers**. The runtime's compositor will stack these together at the very last microsecond, and can even perform **Late-Stage Reprojection (LSR)** to "tweak" your final image if the user's head moved after you finished rendering. + +== Advanced: Sub-Frame Workloads and Latency Monitoring + +While OpenXR handles the frame pacing, Vulkan allows you to implement more granular control: -* **Layers**: These are separate planes of imagery (like the 3D scene, a 2D HUD, or a curved menu) that the runtime's compositor will stack together at the very last microsecond using its own high-priority GPU pass. -* **Pacing**: `xrEndFrame` tells the runtime: "My GPU work is queued. Use the synchronization we agreed upon in the handshake to know when it's safe to show this frame." +* **Coordinating Sub-Frame Workloads**: You can use Vulkan's **Timeline Semaphores** to implement workloads that run asynchronously from the main XR lifecycle. For example, you can kick off a complex visibility or light-baking compute pass before calling `xrWaitFrame`, ensuring the GPU is busy while the CPU waits for the heartbeat. +* **Predicting GPU-side Latency**: The runtime provides the display time, but you can use Vulkan **Query Pools** to measure your own GPU-side latency. This data allows you to dynamically adjust your engine's internal prediction offset, providing a tighter alignment than generic runtime prediction. == The Engine Loop Summary @@ -66,6 +83,6 @@ Our engine's main loop now follows this precise rhythm: 4. **Render**: Use the future time to predict poses, record Vulkan commands, and submit them. 5. **End**: Hand the finished (or queued) layers back to the runtime for display. -By following this rhythm, we ensure our engine is perfectly synchronized with the headset's hardware, providing the smooth and stable experience required for spatial computing. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#xrWaitFrame[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc index 04b1feb47..dcadea60b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/03_display_time_prediction.adoc @@ -3,18 +3,25 @@ The key to a stable and immersive spatial experience is aligning our engine's internal state with the future point in time when the user will actually see the frame. This is where `predictedDisplayTime` from `XrFrameState` comes into play. We must use this timestamp for two critical tasks: **Locating the User's Head (the View)** and **Calculating our Simulation (Physics and Animation)**. -== 1. Locating the User (`xrLocateViews`) +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. -When we render our views, we need to know exactly where the user's eyes will be. If we use the "current" head pose, the frame will appear to lag because the head will have moved further by the time the frame is displayed. This is known as **Motion-to-Photon Latency**. +Correctly utilizing the predicted display time is a mandatory requirement for XR applications. Failing to align your simulation and rendering with this timestamp will lead to "swim" and motion sickness. + +== 1. Locating the User (`xrLocateViews`) -We use `xrLocateViews` to ask the runtime to predict the user's pose for the future `displayTime`. +When we render our views, we need to know exactly where the user's eyes will be. If we use the "current" head pose, the frame will appear to lag. We use `xrLocateViews` to ask the runtime to predict the user's pose for the future `displayTime`. [source,cpp] ---- -XrViewLocateInfo viewLocateInfo{XR_TYPE_VIEW_LOCATE_INFO}; -viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; -viewLocateInfo.displayTime = frameState.predictedDisplayTime; -viewLocateInfo.space = xrAppSpace; +// Predicting where the user will be at the moment of display +XrViewLocateInfo viewLocateInfo{ + .type = XR_TYPE_VIEW_LOCATE_INFO, + .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + .displayTime = frameState.predictedDisplayTime, + .space = xrAppSpace +}; XrViewState viewState{XR_TYPE_VIEW_STATE}; uint32_t viewCountOutput; @@ -22,15 +29,13 @@ std::vector views(viewCount, {XR_TYPE_VIEW}); xrLocateViews(xrSession, &viewLocateInfo, &viewState, viewCount, &viewCountOutput, views.data()); ---- -The resulting `XrView` array contains the predicted **Pose** (position and orientation) and **FOV** (Field of View) for each eye. Note that FOV can change! Some headsets use dynamic lenses that shift based on the user's focus, so we must update our projection matrices *every frame*. +The resulting `XrView` array contains the predicted **Pose** and **FOV**. Note that FOV can change—some headsets use dynamic lenses that shift based on focus, so we must update our projection matrices *every frame* using these values. == 2. Simulation Alignment: Syncing Physics -It's not just the camera that must be predicted; our entire world simulation must be aligned with `predictedDisplayTime`. This includes physics, animations, and particle systems. +It's not just the camera that must be predicted; our entire world simulation (physics, animations, and particle systems) must be aligned with `predictedDisplayTime`. -If we use a different delta time for our simulation (like a fixed 60Hz update) than for our view prediction (which is tied to the 90Hz display), the result will be a subtle but nauseating disconnect. Objects in the world will appear to "jitter" relative to the user's head movement because they are being updated on a different temporal schedule. - -In our engine, we should calculate our simulation's delta time based on the difference between the `predictedDisplayTime` of the current frame and the previous one. +If we use a different delta time for our simulation (like a fixed 60Hz update) than for our view prediction, objects in the world will appear to "jitter" relative to the user's head movement. In our engine, we calculate our simulation's delta time based on the difference between the predictions of the current and previous frames. [source,cpp] ---- @@ -38,6 +43,13 @@ double deltaTime = static_cast(currentPredictedTime - lastPredictedTime) engine.update(deltaTime); ---- +== Advanced: Correlating GPU Time and Physics Poses + +While the runtime provides the best-guess pose, Vulkan allows you to refine the alignment further: + +* **Correlating GPU Execution Time**: You can correlate GPU-side execution time with the OpenXR system clock. This enables you to measure exactly how long your vertex shaders take to transform the scene, allowing for "Dynamic Complexity" scaling to keep your engine's render time perfectly within the predicted display window. +* **Physics-Aware Pose Adjustments**: If the user's head is about to clip through a virtual wall, you can use Vulkan **Compute Shaders** to perform real-time "Pose Clamping," adjusting the OpenXR prediction based on your engine's physics data before the final rendering pass. + == The Danger of Clock Drift A common mistake in XR development is to use `std::chrono::now()` or `glfwGetTime()` to drive simulation. These clocks are decoupled from the XR runtime's heartbeat. @@ -47,10 +59,10 @@ A common mistake in XR development is to use `std::chrono::now()` or `glfwGetTim Even if they start at the same time, they will eventually **Drift**. After 30 minutes of gameplay, your physics engine might be 5ms ahead or behind the display's actual refresh cycle. This drift is what causes "random" stutters and hitches that are impossible to debug if you don't use the `predictedDisplayTime` consistently. -By using only the `predictedDisplayTime` provided by the XR runtime, we ensure that every element of our spatial scene—from the smallest particle to the user's own viewpoint—is perfectly synchronized with the hardware's display strobes. +TIP: For more information on time prediction, check out the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#xrLocateViews[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. == Next Steps: Minimizing Latency -Now that our frame loop is correctly paced and our simulation is aligned with the future, we have established the foundation for a high-quality spatial experience. However, there is still a small window of latency between the end of our command buffer recording and the absolute final moment before submission. In the next chapter, we will explore **Late Latching** and how **Timeline Semaphores** can help us shave off those final microseconds of "swim." +Now that our frame loop is correctly paced and our simulation is aligned with the future, we have established the foundation for a high-quality spatial experience. In the next chapter, we will explore **Late Latching** and how **Timeline Semaphores** can help us shave off those final microseconds of "swim." xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/02_xr_lifecycle.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/05_Predictive_Frame_Loop/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc index dc8688b19..81928ec79 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc @@ -1,47 +1,68 @@ :pp: {plus}{plus} = The Last-Second Update -To implement late latching, we need a mechanism that allows the GPU to sit in a "ready state" for as long as possible without stalling the entire hardware pipeline. In Vulkan 1.3, the combination of **Timeline Semaphores** and **Synchronization 2** provides exactly what we need. +To implement late latching, we need a mechanism that allows the GPU to sit in a "ready state" for as long as possible. In Vulkan 1.3, the combination of **Timeline Semaphores** and **Synchronization 2** provides the perfect toolset. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides the poses, the specific low-latency synchronization technique of "Late Latching" is implemented using Vulkan's core synchronization features. This is a powerful optimization that you implement with Vulkan to achieve better results than a standard integration. == Gating the Pipeline: The Wait-Before-Submit Pattern -Think of a timeline semaphore as a **Digital Gate**. In a standard loop, the CPU prepares everything, then sends it to the GPU. In a "Late Latching" loop, we change the order of operations: +Think of a timeline semaphore as a **Digital Gate**. In a "Late Latching" loop, we change the standard order of operations to minimize the time between pose sampling and pixel display: -1. **CPU Records Commands**: We record our entire command buffer, including all the draw calls for our spatial views. However, we use a placeholder matrix or just "stale" data from the start of the frame. +1. **CPU Records Commands**: We record our entire command buffer using a placeholder matrix or "stale" data from the start of the frame. 2. **CPU Submits Work**: We submit the command buffer to the GPU using `vkQueueSubmit2`. -3. **GPU Waits**: Crucially, we tell the GPU to **WAIT** at the very beginning of the pipeline (usually the Vertex Shader stage) until a specific timeline semaphore reaches a target value. +3. **GPU Waits**: We tell the GPU to **WAIT** at the very beginning of the pipeline (usually the Vertex Shader stage) until a specific timeline semaphore reaches a target value. 4. **CPU Polls**: While the GPU is sitting at the gate, the CPU is free to keep polling OpenXR for the absolute latest head pose. 5. **The Signal**: Only at the last possible microsecond, the CPU updates the matrix in memory and **Signals** the semaphore. The GPU gate opens, and the draw calls begin with the freshest possible data. +[source,cpp] +---- +// Configure the timeline semaphore wait info using designated initializers +vk::TimelineSemaphoreSubmitInfo timelineSubmitInfo{ + .waitSemaphoreValueCount = 1, + .pWaitSemaphoreValues = &nextValue +}; + +// Define the semaphore wait at the Vertex Shader stage +vk::SemaphoreSubmitInfo waitSemaphoreInfo{ + .pNext = &timelineSubmitInfo, + .semaphore = *lateLatchingSemaphore, + .value = nextValue, + .stageMask = vk::PipelineStageFlagBits2::eVertexShader +}; + +// Submit the command buffer +vk::SubmitInfo2 submitInfo{ + .waitSemaphoreInfoCount = 1, + .pWaitSemaphoreInfos = &waitSemaphoreInfo, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &commandBufferInfo +}; + +// The GPU will now stall at the vertex shader until the CPU signals the semaphore +queue.submit2(submitInfo); +---- + == Stage Mask Selection: Where to Wait? A critical performance decision is *where* the GPU should wait. In `vk::SemaphoreSubmitInfo`, we specify a `stageMask`. * **Top of Pipe**: If we wait here, the GPU does nothing. * **Vertex Shader**: If we wait here, the GPU can still perform "Top of Pipe" work like command parsing or index fetching. It only stalls when it needs the actual vertex data. -* **Fragment Shader**: This is usually too late for late latching, as the geometry has already been rasterized based on the (potentially stale) vertex positions. +* **Fragment Shader**: This is usually too late for late latching, as the geometry has already been rasterized based on the vertex positions. -By waiting at the **Vertex Shader** stage, we maximize hardware utilization while still ensuring that our projection and view matrices are as fresh as the hardware allows. +By waiting at the **Vertex Shader** stage, we maximize hardware utilization while ensuring our projection and view matrices are as fresh as the hardware allows. -[source,cpp] ----- -// Configure the timeline semaphore wait for the GPU -vk::TimelineSemaphoreSubmitInfo timelineSubmitInfo; -uint64_t waitValue = currentFrameCount + 1; -timelineSubmitInfo.setWaitSemaphoreValues(waitValue); +== Advanced: Real-Time Scheduling and Uniform Updates -vk::SubmitInfo2 submitInfo; -vk::SemaphoreSubmitInfo waitSemaphoreInfo; -waitSemaphoreInfo.setSemaphore(*lateLatchingSemaphore); -waitSemaphoreInfo.setValue(waitValue); -waitSemaphoreInfo.setStageMask(vk::PipelineStageFlagBits2::eVertexShader); +Vulkan allows us to implement level of late-binding that the OpenXR standard does not natively facilitate: -submitInfo.setWaitSemaphoreInfos(waitSemaphoreInfo); -submitInfo.setCommandBufferInfos(commandBufferInfo); - -// The GPU will now wait at the vertex shader stage until the CPU signals waitValue -queue.submit2(submitInfo); ----- +* **Late-Binding Uniform Updates**: Using Vulkan's **External Host Memory** and **Coherent Mapping**, you can map the buffer containing your transformation matrices directly to CPU-visible memory. This enables the CPU to update the head pose even after the command buffer has been submitted. +* **High-Priority Queues**: While the runtime's compositor is high priority, your application typically is not. You can use Vulkan's **Priority Queues** to request a "High Priority" graphics queue for your late-latching work, ensuring your updates are never preempted by lower-priority background tasks. == Why This Kills "Swim" @@ -49,4 +70,6 @@ queue.submit2(submitInfo); By using the wait-before-submit pattern, we reduce that gap from ~10-15ms down to ~1-2ms. The virtual world feels "anchored" to the physical space because the view matrices being used by the GPU were updated only microseconds before the first vertex was processed. +TIP: For more details on synchronization, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#synchronization[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. + xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc index bbd418549..9f88afc8c 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/03_implementation.adoc @@ -1,63 +1,74 @@ :pp: {plus}{plus} -= Implementation += Late Latching Implementation -The magic of late latching happens in the final moments before the CPU gives the GPU the "go ahead." In our engine's main loop, we've already submitted our command buffers using the `vk::SubmitInfo2` structure with the appropriate timeline semaphore wait. Now, we just need to update the data and signal the semaphore. +The efficiency of late latching depends on the final moments before the CPU signals the GPU. In our engine, we use persistently mapped memory and timeline semaphores to ensure the lowest possible latency. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides the poses, the specific low-latency synchronization technique of "Late Latching" is implemented using Vulkan's core synchronization features. This is a powerful optimization that you implement with Vulkan to achieve better results than a standard integration. == The "Last-Second" View Prediction -To get the absolute latest head pose, we call `xrLocateSpace` using the `predictedDisplayTime` we calculated in the previous chapter. +The magic of late latching happens in the final moments before the CPU gives the GPU the "go ahead." In our engine's main loop, we call `xrLocateSpace` using the `predictedDisplayTime` to get the absolute latest head pose. [source,cpp] ---- // 1. Get the absolute latest head pose from OpenXR -XrSpaceLocation spaceLocation{XR_TYPE_SPACE_LOCATION}; -XrResult result = xrLocateSpace(viewSpace, baseSpace, predictedDisplayTime, &spaceLocation); +XrSpaceLocation spaceLocation{ + .type = XR_TYPE_SPACE_LOCATION +}; +xrLocateSpace(viewSpace, baseSpace, predictedDisplayTime, &spaceLocation); -if (XR_UNQUALIFIED_SUCCESS(result) && (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) { - // 2. Convert XrSpaceLocation to our engine's internal View/Projection matrices +if (spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { + // 2. Convert to matrix and update persistently mapped memory auto latestViewMatrix = convertXrPoseToMatrix(spaceLocation.pose); - - // 3. Directly update the Host-Visible UBO buffer (already persistently mapped) std::memcpy(mappedUboPointer, &latestViewMatrix, sizeof(latestViewMatrix)); - // 4. Flush the memory range if it's not coherent - // This ensures the GPU can see our update immediately - vk::MappedMemoryRange flushRange; - flushRange.setMemory(*uboBufferMemory); - flushRange.setOffset(0); - flushRange.setSize(sizeof(latestViewMatrix)); + // 3. Flush if memory is not host-coherent using designated initializers + vk::MappedMemoryRange flushRange{ + .memory = *uboBufferMemory, + .offset = 0, + .size = sizeof(latestViewMatrix) + }; device.flushMappedMemoryRanges(flushRange); } -// 5. Signal the timeline semaphore to value X, unblocking the GPU -vk::SemaphoreSignalInfo signalInfo; -signalInfo.setSemaphore(*lateLatchingSemaphore); -signalInfo.setValue(waitValue); +// 4. Signal the timeline semaphore to unblock the GPU +vk::SemaphoreSignalInfo signalInfo{ + .semaphore = *lateLatchingSemaphore, + .value = nextValue +}; device.signalSemaphore(signalInfo); ---- == Persistent Mapping: The Efficiency Requirement -Wait, there’s an important architectural detail here: **Persistent Mapping**. For late latching to work effectively, we cannot afford the overhead of calling `vkMapMemory` and `vkUnmapMemory` every frame. +For late latching to work, we cannot afford the overhead of calling `vkMapMemory` and `vkUnmapMemory` every frame. We map the Vulkan buffer once during initialization and keep the CPU-side pointer available for the lifetime of the application. This allows us to use the raw performance of a direct `memcpy` during our time-critical update window. -* **The Concept**: We map the Vulkan buffer once during initialization and keep the CPU-side pointer (`mappedUboPointer`) available for the lifetime of the application. -* **The Why**: Mapping memory is an expensive system call that often involves kernel-mode transitions. In our late latching loop, we have less than a millisecond to perform the update. We need the raw performance of a direct `memcpy`. +While you are updating the memory, the runtime is refreshing its internal sensor fusion model (sampling IMUs at 1000Hz or higher) to provide the most accurate extrapolation for the predicted display time. == The Coherency Question: `vkFlushMappedMemoryRanges` -Not all GPU memory is "Coherent." On some hardware (especially discrete desktop GPUs), the CPU and GPU have separate caches. Even if we `memcpy` the data, the GPU might still see the "stale" version in its cache. +On some hardware (especially discrete desktop GPUs), the CPU and GPU have separate caches. Even if we `memcpy` the data, the GPU might still see the "stale" version in its cache. -If our memory heap doesn't have the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` flag, we must call `vkFlushMappedMemoryRanges`. This forces the CPU cache to write the data out to physical VRAM where the GPU can see it. In our late latching implementation, this flush is the final step before we open the digital gate. +If our memory heap doesn't have the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` flag, we must call `vkFlushMappedMemoryRanges`. This forces the CPU cache to write the data out to physical VRAM where the GPU can see it. + +== Advanced: Speculative Rendering and Transfer Queues + +Vulkan allows us to push the boundaries of late-stage updates: + +* **Speculative Rendering**: You can use Vulkan to record multiple small "potential" command buffers for different predicted poses and use a timeline semaphore to select the correct one at the last moment—ensuring zero-latency alignment even if the CPU update is slightly delayed. +* **Asynchronous Transfer Queues**: Late latching is strictly for tiny data like transformation matrices. If you need to update larger datasets (like animation bone transforms), you can use Vulkan's **Asynchronous Transfer Queues** to stream this data in parallel with your rendering work, bypassing the single-threaded bottlenecks of the OpenXR main loop. == Limitations of Late Latching -This technique is most effective for "fast-moving" data like the head pose or hand tracking data. However, it’s not suitable for updating entire scene hierarchies or large amounts of vertex data. +This technique is most effective for "fast-moving" data like the head pose or hand tracking data. * **Data Size**: Keep your late-latched updates small (usually just a few matrices). * **Timing**: If the update takes too long, the GPU will idle while waiting for the semaphore, which can reduce your overall framerate. -By focusing purely on the view and projection matrices, we keep the overhead minimal and the visual latency as low as the hardware allows. - -With late latching implemented, our virtual environment is now as anchored as possible. In the next chapter, we will shift our focus to **Action Spaces**—how we turn user input (button clicks and hand movements) into meaningful interactions within this spatially-aligned world. +TIP: For more information on memory mapping and persistent buffers, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-device-hostaccess[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/02_last_second_update.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/06_Late_Latching/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc index 87e02de8e..b12264add 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc @@ -1,44 +1,41 @@ :pp: {plus}{plus} = The OpenXR Action System -The OpenXR input system is built around three core concepts: **Action Sets**, **Actions**, and **Interaction Profiles**. By separating our engine's logic from the physical hardware, we ensure that our application remains robust and adaptable across a vast ecosystem of controllers. +The OpenXR input system is built around three core concepts: **Action Sets**, **Actions**, and **Interaction Profiles**. By separating our engine's logic from the physical hardware, we ensure that our application remains robust across a vast ecosystem of controllers. -== The Hardware Abstraction Layer: Thinking in Actions +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. -In a legacy input system, you might write code like `if (buttonA.isPressed()) { jump(); }`. This is a fragile approach in XR, where one user might have an Oculus Touch controller (with an 'A' button) and another might have Valve Index controllers (with a pressure-sensitive trackpad). +Defining input through an action-based system is a mandatory architectural pattern in OpenXR. You must use these abstractions to ensure your application can run on the wide variety of controllers supported by the OpenXR ecosystem. + +== The Hardware Abstraction Layer: Thinking in Actions -OpenXR uses an **Action-Based** model. Instead of looking for buttons, your engine defines what the user can *do*: +In a legacy input system, you might write code like `if (buttonA.isPressed()) { jump(); }`. This is a fragile approach in XR, where hardware varies wildly. OpenXR uses an **Action-Based** model. Instead of looking for buttons, your engine defines what the user can *do*: 1. **Define the Action**: "I need an action called 'Jump'." 2. **Assign the Type**: Is it a boolean (on/off), a float (analog trigger), or a pose (hand position)? -3. **Suggest the Binding**: "On an Oculus controller, map 'Jump' to the 'A' button. On a Vive controller, map it to the 'Trackpad Click'." +3. **Suggest the Binding**: The XR runtime handles the translation at the last possible second, giving your engine a unified stream of events. -The XR runtime handles the "Translation" at the last possible second, giving your engine a clean, unified stream of "Jump" events regardless of the physical hardware. +While the runtime acts as a translation layer—resolving paths like `/user/hand/right/input/grip/pose` to physical sensors—your engine remains focused on intent. == 1. Action Sets: Contextual Input -An **Action Set** is a container for related actions. This allows you to enable or disable groups of inputs based on what the user is doing. - -* **Gameplay Set**: Contains "Teleport", "Grab", and "Shoot". -* **Menu Set**: Contains "Select" and "Back". +An **Action Set** is a container for related actions. This allows you to enable or disable groups of inputs based on what the user is doing (e.g., "Gameplay" vs. "Menu"). [source,cpp] ---- -// 1. Create an Action Set -XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; -std::strcpy(actionSetInfo.actionSetName, "gameplay"); -std::strcpy(actionSetInfo.localizedActionSetName, "Main Gameplay"); -actionSetInfo.priority = 0; - +// 1. Create an Action Set using designated initializers +XrActionSetCreateInfo actionSetInfo{ + .type = XR_TYPE_ACTION_SET_CREATE_INFO, + .actionSetName = "gameplay", + .localizedActionSetName = "Main Gameplay", + .priority = 0 +}; XrActionSet gameplayActionSet; xrCreateActionSet(instance, &actionSetInfo, &gameplayActionSet); ---- -[TIP] -==== -**Localized Names**: The `localizedActionSetName` is important! The XR runtime may show this to the user in a "Controller Binding" menu, allowing them to remap your actions to their own preferences. -==== - == 2. Actions: Defining Intent Within an action set, we define our individual **Actions**. @@ -46,35 +43,43 @@ Within an action set, we define our individual **Actions**. [source,cpp] ---- // 2. Create a Pose Action for hand tracking -XrActionCreateInfo actionInfo{XR_TYPE_ACTION_CREATE_INFO}; -actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT; -std::strcpy(actionInfo.actionName, "hand_pose"); -std::strcpy(actionInfo.localizedActionName, "Hand Pose"); -actionInfo.countSubactionPaths = 0; - +XrActionCreateInfo actionInfo{ + .type = XR_TYPE_ACTION_CREATE_INFO, + .actionName = "hand_pose", + .actionType = XR_ACTION_TYPE_POSE_INPUT, + .localizedActionName = "Hand Pose" +}; XrAction handPoseAction; xrCreateAction(gameplayActionSet, &actionInfo, &handPoseAction); ---- == 3. Interaction Profiles: Suggesting the Map -Finally, we suggest how these actions should map to real hardware. We don't "force" a mapping; we provide a "Suggested Binding" for each known controller type. +Finally, we provide "Suggested Bindings" for known controller types. [source,cpp] ---- -// 3. Map the action to a specific controller path -std::vector bindings; -// We use standardized OpenXR paths like /user/hand/right/input/grip/pose -bindings.push_back({handPoseAction, xrPathToString(instance, "/user/hand/right/input/grip/pose")}); - -XrInteractionProfileSuggestedBinding profileBinding{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; -profileBinding.interactionProfile = xrPathToString(instance, "/interaction_profiles/oculus/touch_controller"); -profileBinding.suggestedBindings = bindings.data(); -profileBinding.countSuggestedBindings = bindings.size(); - +// 3. Map the action to a specific controller path using designated initializers +std::vector bindings = { + { .action = handPoseAction, .binding = xrPathToString(instance, "/user/hand/right/input/grip/pose") } +}; + +XrInteractionProfileSuggestedBinding profileBinding{ + .type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING, + .interactionProfile = xrPathToString(instance, "/interaction_profiles/oculus/touch_controller"), + .countSuggestedBindings = static_cast(bindings.size()), + .suggestedBindings = bindings.data() +}; xrSuggestInteractionProfileBindings(instance, &profileBinding); ---- -This abstraction is especially critical for **Space Manifolds**, where a user's physical movement is converted into a 3D coordinate system for our renderer—the topic of our next section. +== Advanced: GPU-Based Input Filtering and Haptic Synthesis + +Vulkan allows you to implement complex input logic that the OpenXR standard does not natively provide: + +* **GPU-Based Input Filtering**: You can use Vulkan's **Compute Shaders** to process high-frequency pose data streams from OpenXR, performing real-time signal analysis (like noise reduction or gesture intent prediction) directly on the GPU for maximum responsiveness. +* **Physics-Driven Haptics**: While OpenXR haptics are simple vibration commands, you can use Vulkan's **Storage Buffers** to generate complex, physics-driven haptic waveforms in your shaders and stream them to the runtime, creating a tighter link between visual effects and physical feedback. + +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#input[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc index d2930dff7..c983a0b35 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/03_space_manifolds.adoc @@ -3,40 +3,50 @@ While most input actions result in discrete events (like a button press), pose actions are special: they are inherently spatial. To bridge the gap between the user's hand movement and the objects in our 3D engine, we need to create an **Action Space**. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Defining input through an action-based system and managing spatial coordinate systems (Action Spaces) is a mandatory architectural pattern in OpenXR. You must use these abstractions to ensure your application can run on the wide variety of controllers supported by the OpenXR ecosystem. + == Virtual Coordinate Systems: The Action Space Think of an action space as a virtual coordinate system that "follows" the user's hand. In OpenXR, all spatial tracking is relative. To find where a hand is, we must ask: "Where is this Hand Space relative to my World Space?" -1. **Grip Pose**: The location of the user's palm. This is used for holding objects like a sword or a steering wheel. -2. **Aim Pose**: The location and orientation of a ray extending from the controller's front. This is used for pointing, laser-pointers, and UI interaction. +1. **Grip Pose**: The location of the user's palm. Used for holding virtual objects. +2. **Aim Pose**: A ray extending from the controller's front. Used for pointing and UI interaction. [source,cpp] ---- -// 1. Create a Space for our Hand Pose Action -XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; -actionSpaceInfo.action = handPoseAction; -// This allows us to apply an offset (e.g., to align the virtual hand with the physical controller) -actionSpaceInfo.poseInActionSpace = identityPose; +// 1. Create a Space for our Hand Pose Action using designated initializers +XrActionSpaceCreateInfo actionSpaceInfo{ + .type = XR_TYPE_ACTION_SPACE_CREATE_INFO, + .action = handPoseAction, + .poseInActionSpace = identityPose // Can be used to apply a fixed offset +}; XrSpace handSpace; xrCreateActionSpace(session, &actionSpaceInfo, &handSpace); ---- +The runtime handles the world alignment and latency compensation for these spaces, ensuring that the "Hand Space" stays stable relative to the physical sensors. + == Space Manifolds: From Quaternions to Matrices -Every frame, we use `xrLocateSpace` to find where this hand space is located. The runtime provides this as an `XrSpaceLocation`, which contains a position (vector) and an orientation (quaternion). +Every frame, we use `xrLocateSpace` to find the hand's location. The runtime provides this as an `XrSpaceLocation`, containing a position and an orientation. -However, our Vulkan shaders typically expect a **4x4 Matrix** for spatial transformations. This conversion is the "Space Manifold"—mapping the 7D tracking data (3 position + 4 quaternion) into the 16D matrix format used by the GPU's fixed-function vertex hardware. +Our Vulkan shaders expect a **4x4 Matrix**. This conversion is the "Space Manifold"—mapping the 7D tracking data into the 16D matrix format used by the GPU. [source,cpp] ---- -// 2. Locate the hand space relative to the world -XrSpaceLocation location{XR_TYPE_SPACE_LOCATION}; +// 2. Locate the hand space relative to the world coordinate system +XrSpaceLocation location{ + .type = XR_TYPE_SPACE_LOCATION +}; xrLocateSpace(handSpace, worldSpace, predictedDisplayTime, &location); if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { // 3. Convert XrPosef to a 4x4 transformation matrix - // This matrix represents the manifold of the controller's path through the virtual world vk::mat4 handTransform = xrPoseToMatrix(location.pose); // Pass this matrix to our engine's scene graph or directly to a shader @@ -44,12 +54,19 @@ if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { } ---- +== Advanced: Collision-Aware Posing and Smoothing + +Vulkan allows you to refine these poses beyond the raw tracking data provided by OpenXR: + +* **Collision-Aware Posing**: By uploading your scene's bounding volumes to the GPU using **Structured Buffers**, you can use Vulkan to "clamp" the OpenXR-provided poses in real-time, preventing virtual hands from clipping through walls. +* **Temporal Smoothing**: While the runtime filters sensor noise, you can use Vulkan's **Subgroup Operations** to implement highly efficient temporal smoothing or a predictive "heavy" feel for virtual objects. + == Why We Use "App Space" In spatial computing, the **App Space** (or Stage Space) is the origin of our virtual world. By locating the `handSpace` relative to the `worldSpace`, we ensure that if the user walks around their room, the virtual hand stays perfectly aligned with their physical hand relative to the virtual environment. -This transformation process effectively turns a physical controller into a **Space Manifold**—a virtual object that moves through our world's coordinate system. Whether the user is pointing a laser, picking up a sword, or simply waving their hand, the engine sees it all as a single, consistent matrix. +This transformation process turns a physical controller into a virtual object that moves through our world's coordinate system. Whether the user is pointing a laser or simply waving, the engine sees it all as a single, consistent matrix. -In the next chapter, we will look at how to optimize the rendering of these multiple views using **Slang for Spatial Shaders**. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#spaces[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/02_openxr_action_system.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/07_Action_Spaces_Input/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc index 0244eab3a..69cca3d26 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc @@ -1,7 +1,13 @@ :pp: {plus}{plus} = Native Multiview -The core technology behind **Single-Pass Stereo** is **Native Multiview** (`VK_KHR_multiview`). This feature allows a single draw call to be broadcast to multiple layers of an image array. For spatial computing, where we almost always have at least two views (left and right eye), this is a massive performance win. +The technology behind **Single-Pass Stereo** is **Native Multiview** (`VK_KHR_multiview`). This feature allows a single draw call to be broadcast to multiple layers of an image array. For spatial computing, where we almost always have at least two views, this is a massive performance win. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Leveraging Vulkan's native multiview support is the industry-standard way to implement high-performance stereo rendering in OpenXR. It allows you to satisfy the runtime's multi-view requirements with minimum CPU overhead. == The CPU Overhead Win: Why We Hate Loops @@ -9,28 +15,32 @@ To understand the value of multiview, look at the alternative: **The Multi-Pass 1. **Loop**: For each eye (0 to 1): 2. **Bind**: Update the view/projection UBO for that eye. -3. **Draw**: Re-issue all 2,000 draw calls for the scene. +3. **Draw**: Re-issue all draw calls for the scene. -In a complex scene, this means the CPU is doing twice the work (4,000 draw calls total). This often makes the application **CPU-Bound**, meaning the GPU is sitting idle while waiting for the CPU to finish its second loop. +In a complex scene, the CPU does twice the work, often becoming **CPU-Bound**. **Multiview kills the loop.** You issue the draw calls *once*, and the GPU's fixed-function hardware handles the broadcast. The CPU overhead is cut in half, and the GPU can stay saturated. -**Multiview kills the loop.** You issue the 2,000 draw calls *once*, and the GPU's fixed-function hardware handles the duplication for the second eye. The CPU overhead is cut in half, and the GPU can stay saturated. +[source,cpp] +---- +// Enabling Multiview in Vulkan 1.3 using designated initializers +vk::RenderingInfo renderingInfo{ + .renderArea = {{0, 0}, {xrWidth, xrHeight}}, + .layerCount = 1, // Must be 1 when using multiview masks + .viewMask = 0b11, // Broadcast to Layer 0 (Left) and Layer 1 (Right) + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment +}; -== View Masks and the Broadcast Mechanism +commandBuffer.beginRendering(renderingInfo); +---- -In a standard Vulkan pipeline, a draw call targets a specific attachment. With multiview enabled (part of the Vulkan 1.3 core), you specify a **View Mask** in your `vk::RenderingInfo`. +== View Masks and the Broadcast Mechanism -A view mask is a bitmask where each set bit represents a layer in the output attachment array. +In a standard Vulkan pipeline, a draw call targets a specific attachment. With multiview enabled, you specify a **View Mask** in your `vk::RenderingInfo`. A view mask is a bitmask where each set bit represents a layer in the output array. -* **Mask 0b01 (1)**: Renders only to Layer 0. * **Mask 0b11 (3)**: Renders to both Layer 0 (Left Eye) and Layer 1 (Right Eye). * **Mask 0b1111 (15)**: Renders to four layers (used for Quad-Views). -[source,cpp] ----- -// 1. Enabling Multiview in Vulkan 1.3 Dynamic Rendering -vk::RenderingInfo renderingInfo; -renderingInfo.viewMask = 0b11; // Broadcast to bit 0 and bit 1 ----- +By using multiview, you are aligning your engine with how the runtime expects to receive data. Most runtimes prefer **Array-Based Swapchains** because they allow for better hardware-level parallelization and coherent caching between eyes. == The Hardware Implementation: SV_ViewID @@ -38,11 +48,7 @@ When multiview is active, the GPU executes the vertex shader for each bit set in [source,slang] ---- -// 2. Using the View ID in a Slang Shader -struct VertexInput { - float3 position : POSITION; -}; - +// Using the View ID in a Slang Shader struct VertexOutput { float4 position : SV_Position; uint viewID : SV_ViewID; @@ -51,7 +57,6 @@ struct VertexOutput { VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { VertexOutput output; // Load the correct eye matrix using the viewID - // The GPU automatically runs this shader once per viewID (0 and 1) float4x4 mvp = eyeMatrices[viewID]; output.position = mul(mvp, float4(input.position, 1.0)); output.viewID = viewID; @@ -59,8 +64,13 @@ VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { } ---- -It's important to understand that while the vertex work is duplicated, the **Primitive Assembly** and **Rasterization** stages can often be optimized by the hardware. Because the two eyes are looking at the same geometry from slightly different angles, the GPU can reuse vertex cache data, making single-pass stereo much more efficient than two separate passes. +== Advanced: View-Specific Scaling and Occlusion Culling + +Vulkan allows for granular control that goes beyond the standard OpenXR multiview integration: + +* **Per-View Attributes**: Vulkan's **Multiview Per-View Attributes** allow you to use different resolutions or shader attributes for each view index within a single draw call. This enables optimizations like using lower-resolution geometry for peripheral views. +* **Cross-View Dependencies**: You can use Vulkan's **Subgroup Operations** to share data between shader invocations across different views. This enables **Inter-View Occlusion Culling**, where the visibility test from one eye can be shared with the second, significantly reducing redundant work. -In the next section, we'll see how to architect our **Slang** shaders to manage this "N-View" logic efficiently. +TIP: For more information on multiview, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VK_KHR_multiview[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc index 850538635..b355ee902 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/03_slang_architecture.adoc @@ -1,13 +1,19 @@ :pp: {plus}{plus} = Slang Architecture for N-View Shaders -Authoring multiview-aware shaders is a unique challenge. While the GPU's hardware handles the duplication of draw commands, it's our job as shader developers to ensure that we're accessing our eye-specific data—like view and projection matrices—in a way that is both efficient and organized. This is where the **Slang** language really shines. +Authoring multiview-aware shaders is a unique challenge. While the GPU's hardware handles the duplication of draw commands, it's our job to ensure we're accessing our eye-specific data—like view and projection matrices—efficiently. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Using modern shader languages like Slang to manage the multiple views required by OpenXR is a standard practice for high-performance engines. This architecture allows you to handle an arbitrary number of views (mono, stereo, or quad) with a single, unified shader codebase. == The N-View Mindset: Agnostic Shaders -In a traditional shader, you might pass a single `viewMatrix`. In an XR-aware engine, we must adopt an **N-View Mindset**. We write our shaders once, assuming they might be used for mono, stereo, or even "Quad-Views" (4 eyes). +In a traditional shader, you might pass a single `viewMatrix`. In an XR-aware engine, we adopt an **N-View Mindset**, writing our shaders to handle mono, stereo, or even "Quad-Views" (4 eyes) dynamically. -Slang's powerful type system and **StructuredBuffers** allow us to pass an array of view data that the shader can index into dynamically based on which eye is currently being processed. +Slang's powerful type system and **StructuredBuffers** allow us to pass an array of view data that the shader can index into based on the `SV_ViewID`. [source,slang] ---- @@ -19,13 +25,8 @@ struct SpatialView { }; // 2. Accessing N-Views in a Structured Buffer -// This buffer contains data for all active views (e.g., Left, Right, Inset Left, Inset Right) StructuredBuffer eyeViews; -struct VertexInput { - float3 position : POSITION; -}; - struct VertexOutput { float4 position : SV_Position; uint viewID : SV_ViewID; @@ -34,8 +35,7 @@ struct VertexOutput { VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { VertexOutput output; - // Slang makes it easy to access the current eye view data - // The compiler and hardware ensure that 'viewID' matches the current broadcast layer + // Index into our views using the hardware-provided viewID SpatialView currentView = eyeViews[viewID]; output.position = mul(currentView.viewProjection, float4(input.position, 1.0)); @@ -44,18 +44,44 @@ VertexOutput main(VertexInput input, uint viewID : SV_ViewID) { } ---- +On the C++ side, we use designated initializers to bind these buffers to our descriptors: + +[source,cpp] +---- +vk::DescriptorBufferInfo bufferInfo{ + .buffer = *eyeViewsBuffer, + .offset = 0, + .range = sizeof(SpatialView) * maxViews +}; + +vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *descriptorSet, + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStructuredBuffer, + .pBufferInfo = &bufferInfo +}; +---- + == Propagation to the Fragment Shader -The `SV_ViewID` isn't just for the vertex shader. You can propagate it down to the **Fragment Shader** to perform eye-specific logic. +The `SV_ViewID` isn't just for the vertex shader. You can propagate it to the **Fragment Shader** to perform eye-specific logic, such as calculating specular highlights based on each eye's physical location. + +The runtime handles the view mask routing and viewport array management, ensuring each fragment shader invocation targets the correct layer of your multiview broadcast. Many runtimes will also optimize small buffers by keeping them in the GPU's "Constant Cache," making per-view indexing extremely efficient. + +== Advanced: Cooperative Shading and Dynamic Projection + +Vulkan allows us to push the "N-View" mindset further than the standard: -Why would you do this? Consider **Specular Highlights**. Since each eye is at a slightly different physical location, the reflections on a shiny surface should be slightly different for each eye. By using the `eyePosition` from our `SpatialView` structure (indexed by `viewID`), we can calculate the correct view-dependent lighting for each eye in a single pass. +* **Cooperative View Shading**: While you can branch on `viewID`, doing so can lead to GPU divergence. You can use Vulkan's **Subgroup Operations** to share information between views, maintaining high GPU occupancy. +* **Dynamic Projection Models**: By using **Shader Objects** (`VK_EXT_shader_object`), you can dynamically swap view-projection logic without re-creating the entire pipeline, allowing for real-time adjustments to lens-specific projection models. == Modular Spatial Shaders -Slang's support for **Generics** and **Interfaces** means we can write shaders that are completely agnostic of the number of views. We can define our spatial logic once and then let the compiler handle the underlying details of the multiview integration. +Slang's support for **Generics** and **Interfaces** means we can write shaders that are completely agnostic of the number of views. We define our spatial logic once and let the compiler handle the underlying multiview integration. -This significantly reduces the chances of errors. For example, if you add a third view for a "Spectator Camera" or a "Rear View Mirror," you don't need to change your shader code—you just update your view mask and the size of your `eyeViews` buffer. +This significantly reduces the chances of errors. If you add a third view for a "Spectator Camera," you don't need to change your shader code—just update your view mask and the size of your `eyeViews` buffer. -In the next chapter, we'll see how to push this "N-View" mindset even further by looking at **Quad-Views** and **Foveated Rendering**. +TIP: For more information on Slang and Vulkan integration, check out the official link:https://shader-slang.com/slang/user-guide/index.html[Slang User Guide], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/02_native_multiview.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/08_Slang_Spatial_Shaders/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc index 820ea84a4..25983952e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc @@ -1,60 +1,68 @@ :pp: {plus}{plus} = Primary Stereo with Insets -In a **Quad-View** architecture, each eye is actually seeing two overlapping images. The **Primary View** is a wide, lower-resolution image that covers the user's entire field-of-view (FOV). The **Inset View** is a smaller, high-resolution image that is centered around the user's gaze. +In a **Quad-View** architecture, each eye sees two overlapping images. The **Primary View** is a wide, lower-resolution image for the periphery, and the **Inset View** is a smaller, high-resolution image centered on the user's gaze. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Configuring quadruple view configurations for foveated rendering is a standard process in OpenXR for supported hardware. You must use the runtime's view configuration APIs and Vulkan's multiview features to correctly target both primary and inset views. == The Pixel Density Problem -To understand why we use quad-views, we must consider the hardware limits of modern displays. A typical VR headset might have a 4K resolution per eye spread over a 110-degree FOV. +To understand quad-views, consider the hardware limits of modern displays. A typical 4K-per-eye headset spread over a 110-degree FOV provides roughly 20-25 **PPD** (Pixels Per Degree). Human "Retina" resolution is closer to 60 PPD. -* **Result**: This gives a density of roughly 20-25 **PPD** (Pixels Per Degree). -* **The Goal**: Human "Retina" resolution is closer to 60 PPD. +Instead of doubling the resolution of the entire display, we use **Foveated Rendering**. We render a high-density "Inset" (60 PPD) where the eye is looking, and a lower-density "Primary" (20 PPD) for the periphery. -We cannot simply double the resolution of the entire display—the GPU couldn't handle it. Instead, we use **Foveated Rendering**. We render a high-density "Inset" (60 PPD) only where the eye is looking, and a lower-density "Primary" (20 PPD) for the periphery. +The runtime handles the complex fusion of IR eye-tracking sensors and frame timing to align these insets, while automatically "feathering" the edges to prevent a jarring border. == Fixed vs. Dynamic Foveation -1. **Fixed Foveation (FFR)**: The high-detail inset is locked to the center of the lens. This is effective because XR lenses are naturally blurrier at the edges anyway. -2. **Dynamic Foveation (DFR)**: The high-detail inset moves in real-time based on **Eye Tracking** data. This is the "Holy Grail" of XR performance, as the user always sees perfect detail regardless of where they look. +1. **Fixed Foveation (FFR)**: The high-detail inset is locked to the center of the lens. +2. **Dynamic Foveation (DFR)**: The high-detail inset moves in real-time based on eye tracking. [source,cpp] ---- -// 1. Defining the View Config for Quad-Views (N=4) -// Indices 0,1 are the wide base views. -// Indices 2,3 are the high-detail foveal insets. -XrViewConfigurationView configViews[4]; +// Defining the View Config for Quad-Views (N=4) using designated initializers +// Indices 0,1 are the wide base views; 2,3 are high-detail foveal insets. +XrViewConfigurationView configViews[4] = { + { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW, .recommendedImageRectWidth = 1920, .recommendedImageRectHeight = 1080 }, + { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW, .recommendedImageRectWidth = 1920, .recommendedImageRectHeight = 1080 }, + { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW, .recommendedImageRectWidth = 1024, .recommendedImageRectHeight = 1024 }, + { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW, .recommendedImageRectWidth = 1024, .recommendedImageRectHeight = 1024 } +}; ---- == Rendering the Quad-View in Slang -From a rendering perspective, our engine manages four distinct view matrices. The inset views (2 and 3) have a much smaller FOV, which effectively "zooms in" on the scene without changing the camera position. +Our engine manages four distinct view matrices. In Slang, we handle the broadcast to all four layers in a single pass: [source,slang] ---- -// 2. Slang Shader Handling N=4 Views +// Slang Shader Handling N=4 Views struct QuadViewData { float4x4 viewProjection; - float4 viewportScale; // Scale factor for foveal inset }; StructuredBuffer allViews; // 4 Views total -struct VertexOutput { - float4 position : SV_Position; - uint viewID : SV_ViewID; -}; - VertexOutput main(uint viewID : SV_ViewID, float3 pos : POSITION) { VertexOutput output; - QuadViewData current = allViews[viewID]; - // The GPU broadcasts the same geometry to 4 different viewports/layers - output.position = mul(current.viewProjection, float4(pos, 1.0)); + output.position = mul(allViews[viewID].viewProjection, float4(pos, 1.0)); output.viewID = viewID; return output; } ---- -Crucially, the **Inset View** isn't just a zoomed-in version of the base view. It's a high-density image that captures more detail from the 3D scene. We are effectively focusing the GPU's rasterization power on a smaller subset of the world. +== Advanced: Dynamic Inset Scaling and VRS + +Vulkan allows you to push foveated rendering further than the OpenXR quad-view standard: + +* **Fragment Density Maps**: While OpenXR expects fixed swapchain sizes, you can use Vulkan's **Fragment Density Maps** to "fake" a resized inset. By rendering to a smaller sub-region and using a density map to scale during sampling, you can adjust foveal resolution without swapchain recreation. +* **Variable Rate Shading (VRS)**: You can use Vulkan's VRS to maintain high-resolution geometry for the periphery while reducing the shading cost, ensuring geometric edges remain sharp even when fragment density is low. + +TIP: For more details on foveated rendering and quad-views, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_VARJO_quad_views[XR_VARJO_quad_views Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc index 57baf5fe8..a57ae45a2 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/03_multi_layer_composition.adoc @@ -3,47 +3,78 @@ Rendering a **Quad-View** is only half the battle. Once we have our four views (base eyes and high-detail insets), we need to get them to the **OpenXR Compositor** for the final display. This is where **Multi-Layer Composition** comes in. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Submitting multiple composition layers is a standard process in OpenXR. You must use the runtime's layer APIs and Vulkan's synchronization primitives to correctly hand off each projection layer to the compositor. + == The Layer Stack: Modular Imagery -In OpenXR, you don't just submit a single image. You submit an array of **Composition Layers**. Think of the compositor as a "Super-GPU" that performs a final, high-priority pass to stack these images together before they hit the display. +In OpenXR, you don't just submit a single image. You submit an array of **Composition Layers**. The compositor performs a final, high-priority pass to stack these images together before they hit the display. -1. **Base Layer**: Your standard 3D world (usually indices 0 and 1 of your quad-view). +1. **Base Layer**: Your standard 3D world (indices 0 and 1 of your quad-view). 2. **Inset Layer**: The high-detail foveal data (indices 2 and 3). -3. **Overlay Layer**: A flat HUD or menu that stays perfectly sharp, regardless of the 3D scene's resolution. +3. **Overlay Layer**: A flat HUD or menu that stays perfectly sharp. [source,cpp] ---- -// 1. Defining Composition Layers in OpenXR -XrCompositionLayerProjection baseLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; -XrCompositionLayerProjection insetLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; - -// 2. Submitting both layers to xrEndFrame -// The order matters: the last layer in the list is drawn on top. -std::vector layers; -layers.push_back(reinterpret_cast(&baseLayer)); -layers.push_back(reinterpret_cast(&insetLayer)); - -XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; -frameEndInfo.layerCount = layers.size(); -frameEndInfo.layers = layers.data(); +// 1. Defining Composition Layers in OpenXR using designated initializers +XrCompositionLayerProjection baseLayer{ + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, + .layerFlags = XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT, + .space = worldSpace, + .viewCount = 2, + .views = baseViewsPtr +}; + +XrCompositionLayerProjection insetLayer{ + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, + .layerFlags = XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT, + .space = worldSpace, + .viewCount = 2, + .views = insetViewsPtr +}; + +// 2. Submitting layers to xrEndFrame +std::vector layers = { + reinterpret_cast(&baseLayer), + reinterpret_cast(&insetLayer) +}; + +XrFrameEndInfo frameEndInfo{ + .type = XR_TYPE_FRAME_END_INFO, + .displayTime = frameState.predictedDisplayTime, + .layerCount = static_cast(layers.size()), + .layers = layers.data() +}; xrEndFrame(session, &frameEndInfo); ---- +The order matters: the last layer in the list is drawn on top. + == Masking and Blending -How does the compositor know where to show the high-detail inset? Each layer specifies a **Projection View**. For quad-views, the inset layer's views will have a smaller FOV but the same camera pose. +How does the compositor know where to show the high-detail inset? Each layer specifies a **Projection View**. For quad-views, the inset layer's views have a smaller FOV but the same camera pose. The compositor uses the FOV tangents to mathematically "place" the high-detail image over the low-detail one. It then performs a **Soft Edge Blend** to ensure the transition between the 20 PPD and 60 PPD regions is invisible to the user. +== Advanced: Physically-Based Compositing and Alpha Blending + +Vulkan allows you to perform your own depth-aware layer merging beyond the runtime's defaults: + +* **Custom Layer Merging**: While runtimes are often limited to a few layers, you can use Vulkan's **Subgroup Operations** and **Input Attachments** to merge your own layers into a single consolidated image before hand-off, bypassing runtime restrictions. +* **Shared Depth Buffers**: Unless you use specific extensions, the runtime doesn't know about the depth values of your layers. You can use Vulkan to create a **Shared Depth Buffer** across all composition layers, enabling perfect per-pixel occlusion between your HUD, world, and insets. + == Resource Synchronization: Multi-Swapchain Pacing From an engine perspective, this means we are now managing **Multiple Swapchains** per frame. * **Pacing**: We must call `xrWaitSwapchainImage` and `xrAcquireSwapchainImage` for *each* swapchain (Base and Inset). -* **Vulkan Sync**: Our command buffer recording must ensure that all four views are rendered and transitioned before we call `xrReleaseSwapchainImage`. +* **Vulkan Sync**: Our command buffer recording must ensure all four views are rendered and transitioned before calling `xrReleaseSwapchainImage`. -This multi-layer approach is incredibly flexible. Each layer can even have its own resolution and bit-depth, allowing the compositor to optimize the final image based on the specific needs of each part of the scene. +This multi-layer approach allows each layer to have its own resolution and bit-depth, letting the compositor optimize the final image based on the specific needs of each part of the scene. -In the next chapter, we'll see how to push our performance even further by looking at **Variable Rate Shading** (VRS). +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#composition_layers[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/02_primary_stereo_with_insets.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/09_Quad_Views_Foveated/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc index bdb7098fe..467c68872 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc @@ -1,44 +1,56 @@ :pp: {plus}{plus} -= Fragment Density Control and VRS Extensions += Fragment Density Control & VRS -In legacy pipelines, reducing shading quality often meant rendering to a lower-resolution buffer and upscaling, which introduced blurring and aliasing artifacts. Modern Vulkan provides a much more elegant solution via **Variable Rate Shading (VRS)** extensions. +Variable Rate Shading (VRS) allows us to reduce shading cost while maintaining geometric sharpness by decoupling the shading rate from the rasterization rate. -== The Concept: Decoupling Rasterization from Shading +== Ecosystem Perspective -To understand VRS, you must distinguish between two parts of the graphics pipeline: +This chapter falls under the category: **Beyond the OpenXR Standard**. -1. **Rasterization**: The process of determining which pixels are covered by a triangle. -2. **Shading**: The process of running a fragment shader to determine the color of those pixels. +While Vulkan provides the powerful VRS extensions required for foveated shading, the OpenXR standard does not yet mandate their use or coordinate the generation of shading rate maps. You use Vulkan to implement these optimizations to push your engine's performance beyond standard runtime expectations. -In a standard pipeline, they are 1:1. For every pixel covered, you run one fragment shader. With **VRS**, you can run one fragment shader for a *group* of pixels (e.g., a 2x2 or 4x4 block) while still keeping the high-resolution edges and depth data from the rasterizer. +== The Concept: Decoupling Rasterization from Shading -This allows us to maintain **Geometric Sharpness** (no aliasing on object edges) while significantly reducing the **ALU** (Arithmetic Logic Unit) cost of complex lighting and materials in areas where detail isn't needed. +In legacy pipelines, reducing shading quality often meant rendering to a lower-resolution buffer and upscaling, which introduced blurring. Modern Vulkan provides a much more elegant solution via **Variable Rate Shading (VRS)**. -== Shading Rate Images: The Density Map +1. **Rasterization**: Determining which pixels are covered by a triangle. +2. **Shading**: Running a fragment shader to determine the color of those pixels. -In a spatial pipeline, we use a **Shading Rate Image**. This is a low-resolution attachment (usually 1/16th the size of the main view) where each "pixel" represents a tile of the screen. +With **VRS**, you can run one fragment shader for a *group* of pixels (e.g., a 2x2 or 4x4 block) while still keeping the high-resolution edges and depth data from the rasterizer. This maintains **Geometric Sharpness** while significantly reducing the **ALU** cost of complex lighting. + +== Shading Rate Images: The Density Map -* **Value 0x0**: 1x1 shading (Full quality). -* **Value 0x5**: 2x2 shading (1/4th the cost). -* **Value 0xA**: 4x4 shading (1/16th the cost). +In a spatial pipeline, we use a **Shading Rate Image**. This is a low-resolution attachment where each "pixel" represents a tile of the screen (e.g., 8x8 or 16x16). [source,cpp] ---- -// Configuring the shading rate state for a spatial pipeline -vk::PipelineFragmentShadingRateStateCreateInfoKHR shadingRateState{}; -shadingRateState.fragmentSize = {1, 1}; // Default fallback -// We "Replace" the pipeline rate with the rate from our density image -shadingRateState.combinerOps[0] = vk::FragmentShadingRateCombinerOpKHR::eReplace; -shadingRateState.combinerOps[1] = vk::FragmentShadingRateCombinerOpKHR::eKeep; - -vk::GraphicsPipelineCreateInfo pipelineInfo{}; -pipelineInfo.pNext = &shadingRateState; +// Configuring the shading rate state using designated initializers +vk::PipelineFragmentShadingRateStateCreateInfoKHR shadingRateState{ + .fragmentSize = {1, 1}, // Default fallback + .combinerOps = { + vk::FragmentShadingRateCombinerOpKHR::eReplace, // Use the image's rate + vk::FragmentShadingRateCombinerOpKHR::eKeep + } +}; + +vk::GraphicsPipelineCreateInfo pipelineInfo{ + .pNext = &shadingRateState +}; ---- +The runtime checks your hardware's supported tile sizes and ensures your density map matches these limits. It may even "warp" your density map during reprojection to keep the high-resolution "Sweet Spot" aligned with the lens optics. + +== Advanced: Temporal Stability and Async Compute + +Vulkan allows us to push VRS further than the OpenXR standard: + +* **Temporal Shading Stability**: You can use Vulkan's **Subgroup Operations** in a compute shader to analyze the previous frame's depth and motion vectors, generating a refined density map that prevents "shimmering" artifacts in the periphery. +* **Async Compute Coordination**: You can use Vulkan to perform heavy compute work that respects the shading rate map of the primary graphics pass, ensuring your volumetric or lighting calculations are optimized alongside your geometry. + == Fixed Vignette Shading -Because XR lenses are naturally bluer and more distorted at the edges, we can pre-calculate a "Vignette" map. The center of the lens (the "Sweet Spot") is set to 1x1, while the outer rings are set to 2x2 or 4x4. Since the user rarely looks at the extreme edges of the lens, this is essentially "Free Performance." +Because XR lenses are naturally bluer and more distorted at the edges, we can pre-calculate a "Vignette" map. The center of the lens is set to 1x1, while the outer rings are set to 2x2 or 4x4. Since the user rarely looks at the extreme edges, this is essentially "Free Performance." -When combined with **Multiview**, a single shading rate map can be applied to both eyes simultaneously, saving massive amounts of GPU time without affecting the user's immersion. +TIP: For more information on VRS and density maps, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#primsrast-fragment-shading-rate[Vulkan Specification on VRS], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc index 18df42f86..bdcb02691 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/03_gaze_driven_logic.adoc @@ -3,11 +3,19 @@ While static peripheral optimization is a great baseline, the real power of foveated rendering comes when we align the high-detail region with the user's actual gaze. This is known as **Dynamic Foveated Rendering (DFR)**. To achieve this, we ingest eye-tracking data from the OpenXR runtime via the **XR_EXT_eye_gaze_interaction** extension. +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides the raw eye-tracking telemetry via extensions, the complex process of projecting that data into 2D viewport coordinates and updating shading rate maps in real-time is a Vulkan-driven optimization. You use Vulkan to bridge the gap between the runtime's input and your engine's shading density. + == The Biological Exploit: Saccadic Suppression -To understand why DFR is so effective, we must look at how the human brain processes visual information. Our eyes are constantly making tiny, rapid movements called **Saccades**. During a saccade, the brain actually "blanks out" visual input for a fraction of a second—a phenomenon called **Saccadic Suppression**. +To understand why Dynamic Foveated Rendering (DFR) is so effective, we must look at how the human brain processes visual information. Our eyes are constantly making tiny, rapid movements called **Saccades**. During a saccade, the brain "blanks out" visual input—a phenomenon called **Saccadic Suppression**. + +DFR exploits this. By using high-speed eye tracking (usually 120Hz or higher), our engine can shift the high-detail foveal region to the user's new gaze point during the saccade itself. The user never "catches" the low-resolution periphery because their brain ignores visual input while the eye is moving. -DFR exploits this. By using high-speed eye tracking (usually 120Hz or higher), our engine can shift the high-detail foveal region to the user's new gaze point during the saccade itself. The user never "catches" the low-resolution periphery because their brain is literally ignoring visual input while the eye is moving. +While your engine processes these updates, the runtime is sampling internal IR cameras at high frequency and applying the user's personal calibration to report the most accurate `XrEyeGaze` pose possible. == Implementing Gaze-Driven VRS @@ -15,15 +23,13 @@ The process involves three steps: 1. **Ingestion**: Querying the `XrEyeGaze` pose to find where the user is looking in 3D space. 2. **Projection**: Projecting that 3D gaze vector onto the 2D plane of the viewport to find the "foveal center" in UV coordinates. -3. **Update**: Regenerating or shifting the **Shading Rate Image** so that the 1x1 shading region follows that center. +3. **Update**: Regenerating or shifting the **Shading Rate Image** using a compute shader. [source,cpp] ---- // Updating the shading rate image based on projected gaze coordinates void updateShadingRateMap(vk::raii::CommandBuffer& cmd, const glm::vec2& gazeCenter) { // We use a simple compute shader to fill a small R8_UINT texture. - // The shader calculates distance from gazeCenter and assigns: - // 0x0 (1x1 shading), 0x4 (2x1), 0x5 (2x2), etc. cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *gazeVrsPipeline); cmd.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eCompute, 0, gazeCenter); cmd.dispatch(shadingRateWidth / 8, shadingRateHeight / 8, 1); @@ -58,15 +64,19 @@ void main() { } ---- +== Advanced: Predictive Gaze and Conditional Rendering -== Latency: The Immersion Killer +Vulkan allows you to implement proactive optimizations beyond the standard gaze interaction: + +* **Predictive Gaze Smoothing**: Using Vulkan's **ML Inference** extensions, you can analyze the history of gaze points to predict the next fixation point during a saccade, pre-updating your shading rate map to minimize perceived latency. +* **Blink-Aware Optimization**: If the user blinks, you can use Vulkan's **Conditional Rendering** to instantly skip the rendering of the high-detail foveal inset, saving GPU power during the blink event. -Human eyes move incredibly fast. If your eye-tracking telemetry or your shading rate update has too much latency, the user will see the blurriness at the edge of their vision before the high-detail patch "catches up." +== Latency: The Immersion Killer -This is why **Late Latching** (which we covered in Chapter 6) is equally important here. We want to sample the eye gaze at the absolute last microsecond before we signal the timeline semaphore to start the GPU work. By minimizing the latency between "where the eye is" and "where the pixels are sharp," we maintain the illusion of a perfectly high-resolution world. +If your eye-tracking telemetry or shading rate update has too much latency, the user will see the blurriness at the edge of their vision before the high-detail patch "catches up." -When implemented correctly, DFR allows you to push the boundaries of visual fidelity, enabling effects like heavy path-tracing or complex volumetrics that would be impossible to run at full resolution across the entire screen. +This is why **Late Latching** (Chapter 6) is critical. We want to sample the eye gaze at the absolute last microsecond before we signal the timeline semaphore to start the GPU work. By minimizing this gap, we maintain the illusion of a perfectly high-resolution world. -In the next chapter, we will shift from software-driven optimization to hardware-driven geometry by looking at **Canted Displays and Asymmetric Frustums**. +TIP: For more information on eye tracking, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_EXT_eye_gaze_interaction[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/02_fragment_density_control.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/10_Variable_Rate_Shading/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc index 040bbbd30..d1281f5d0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/02_non_parallel_projections.adoc @@ -1,29 +1,37 @@ :pp: {plus}{plus} -= Calculating Asymmetric Frustums for Wide-FOV Lenses += Asymmetric Frustums for Canted Displays -When you query `xrLocateViews`, the OpenXR runtime returns two pieces of information for each eye: a `pose` (position and orientation) and a `fov` (field of view). While many developers are tempted to simply use a fixed FOV and an eye-offset, this approach will fail on headsets with canted lenses. +When you query `xrLocateViews`, OpenXR returns a `pose` and a `fov` for each eye. On headsets with canted (rotated) lenses, these values are unique per eye and require specialized matrix construction. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Correctly calculating projection matrices from runtime-provided FOV tangents is a mandatory step for supporting modern wide-FOV headsets. You must use the runtime's view metadata to ensure your Vulkan renders align with the physical hardware. == The Tangent Frustum: Mapping Angles to Pixels -To understand why OpenXR uses tangents, we must look at how a camera sees the world. A standard field of view is just an angle. But GPUs work with **Clip Space**—a box from -1 to 1. To get from a 3D world to a 2D box, we need the tangents of the FOV angles. +To understand why OpenXR uses tangents, we must look at how a camera sees the world. GPUs work with **Clip Space**—a box from -1 to 1. To get from a 3D world to a 2D box, we need the tangents of the FOV angles. -The `fov` provided by OpenXR is expressed as four tangents: `angleLeft`, `angleRight`, `angleUp`, and `angleDown`. +The `fov` provided by `xrLocateViews` is expressed as four tangents: `angleLeft`, `angleRight`, `angleUp`, and `angleDown`. * **Symmetric (Desktop)**: Left = -Right, Up = -Down. The optical center is in the middle. * **Asymmetric (XR)**: Left != -Right. The optical center is shifted to align with the physical lens. +The runtime calculates these tangents based on the precise factory calibration of the headset, accounting for display panel tilt and lens-eye distance. + == Building the Matrix -To build a proper 4x4 projection matrix from these tangents using `vulkan-hpp`, we use the following logic: +To build a proper 4x4 projection matrix from these tangents, we use the following logic: [source,cpp] ---- // Constructing an asymmetric projection matrix from OpenXR FOV tangents glm::mat4 createProjectionMatrix(const XrFovf& fov, float zNear, float zFar) { - const float tanLeft = tan(fov.angleLeft); - const float tanRight = tan(fov.angleRight); - const float tanUp = tan(fov.angleUp); - const float tanDown = tan(fov.angleDown); + const float tanLeft = std::tan(fov.angleLeft); + const float tanRight = std::tan(fov.angleRight); + const float tanUp = std::tan(fov.angleUp); + const float tanDown = std::tan(fov.angleDown); const float tanWidth = tanRight - tanLeft; const float tanHeight = tanUp - tanDown; @@ -32,7 +40,7 @@ glm::mat4 createProjectionMatrix(const XrFovf& fov, float zNear, float zFar) { projection[0][0] = 2.0f / tanWidth; projection[1][1] = 2.0f / tanHeight; - // The "Shift" components: These align the frustum with the lens optical center + // The "Shift" components align the frustum with the lens optical center projection[2][0] = (tanRight + tanLeft) / tanWidth; projection[2][1] = (tanUp + tanDown) / tanHeight; @@ -45,8 +53,17 @@ glm::mat4 createProjectionMatrix(const XrFovf& fov, float zNear, float zFar) { == Canted Displays: The Rotation Problem -On headsets like the Valve Index or Pimax, the display panels themselves are rotated outward (canted). This means your **View Matrices** (the eye's position and rotation) are also different for each eye. +On headsets like the Valve Index, the display panels are rotated outward (canted). This means your **View Matrices** are also different for each eye. + +When you use **Multiview**, you must push unique projection and view matrices to your shaders. If you try to use a single "stereo" matrix with a fixed offset, the world will appear warped because the GPU is drawing a rectilinear world for a non-rectilinear display. + +== Advanced: Perspective-Correct Interpolation and Dynamic FOV + +Vulkan allows for geometric precision that raw OpenXR data does not manage: + +* **Perspective-Correct Interpolation**: Using Vulkan's **Dynamic State** and **Push Constants**, you can pass eye-specific clip-plane data to your shaders, ensuring lighting and shadow calculations remain stable even on headsets with extreme panel rotation. +* **Viewport Swizzling**: You can use Vulkan's **Viewport Swizzling** to dynamically adjust the rendered area for each eye based on real-time optical changes, providing a more responsive experience than the fixed OpenXR view configuration. -When you use **Multiview**, you must push an array of projection matrices and an array of view matrices to your Slang shaders. If you try to use a single "stereo" matrix with a fixed offset, the world will appear warped and uncomfortable because the GPU is drawing a rectilinear world for a non-rectilinear display. +TIP: For more information on asymmetric projections, check out the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#projection-matrices[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/11_Canted_Displays/03_viewport_swizzling.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc index c9933c335..7929f8cae 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/02_projector_based_spatial_tech.adoc @@ -1,17 +1,23 @@ :pp: {plus}{plus} -= Architecting for Projector-Based Spatial Environments += Projector-Based Spatial Architecture -When we transition from a headset to a **CAVE** (Cave Automatic Virtual Environment), the way we define our "cameras" changes fundamentally. In a headset, the cameras are relative to the user's head. In a CAVE, the "cameras" are fixed physical surfaces—the walls—and the user moves *within* them. +When transitioning from a headset to a **CAVE** (Cave Automatic Virtual Environment), the "cameras" become fixed physical surfaces—the walls—and the user moves *within* them. This requires highly specialized **Off-Axis Projection** logic. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +Standard HMD runtimes are not designed for fixed-surface CAVE environments. You use Vulkan's low-level control over frustums and multi-device synchronization to implement these specialized spatial architectures, often bypassing standard HMD tracking models in favor of custom cluster-based rendering. == The Observer Paradox: One Perspective, Many Screens To understand CAVE rendering, you must accept a paradox: **The 3D scene is only correct for one person.** -Because the walls are fixed, if the user moves to the left, the image on the front wall must be skewed to maintain the illusion of depth from their new perspective. This is known as **Off-Axis Projection**. +Because the physical walls are fixed, if the user moves to the left, the image on the front wall must be skewed to maintain the illusion of depth from their new perspective. This is known as **Off-Axis Projection**. 1. **Fixed Display**: The projection surface (the wall) never moves. 2. **Moving Eye**: The user's eye (the viewpoint) moves constantly. -3. **Result**: The "Pyramid of Vision" (the frustum) is highly asymmetric and changes its skew every frame based on the user's physical location in the room. +3. **Result**: The "Pyramid of Vision" (the frustum) is highly asymmetric and changes its skew every frame based on the user's location. == Calculating the Off-Axis Frustum @@ -19,29 +25,42 @@ To calculate this, we treat the physical wall as the "Near Plane" of our camera. [source,cpp] ---- -// A simple representation of a physical projection wall in our engine +// A representation of a physical projection wall in our engine using designated initializers struct ProjectionWall { glm::vec3 bottomLeft; glm::vec3 bottomRight; glm::vec3 topLeft; - // We calculate the normal and plane equation from these points }; -// The resulting Projection Matrix is built by measuring the distance -// from the eye to the wall's corners in the wall's local coordinate system. +// Configure the rendering info for a specific wall view +vk::RenderingInfo wallRenderingInfo{ + .renderArea = {{0, 0}, {wallWidth, wallHeight}}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachment +}; ---- +The runtime (or a wrapper like Monado) ensures each node only renders the specific `viewID` it is responsible for, while correcting for projector keystoning via a "Warp and Blend" mesh. + == The Distributed Rendering Model: Leader and Follower -Driving a 5-sided CAVE at 4K resolution requires immense GPU power. Most systems use a **Cluster** of computers. +Driving a 5-sided CAVE at 4K resolution requires immense GPU power, typically using a **Cluster** of computers. + +* **Leader Node**: Runs the main simulation, physics, and input processing, broadcasting the world state to followers. +* **Follower Nodes**: Perform **Deterministic Simulation**, updating their world state to match the leader and rendering only their assigned wall. + +== Advanced: Multi-Device Sync and Color Balancing + +Vulkan allows for a level of coordination that standard XR APIs do not natively manage: -* **Leader Node**: Runs the main simulation, physics, and input processing. It then broadcasts the "State of the World" (object positions, user pose, system time) to the followers. -* **Follower Nodes**: These nodes perform **Deterministic Simulation**. They receive the packet, update their internal world state to match the leader exactly, and then render only the specific wall or projector they are responsible for. +* **Multi-Device Buffer Swaps**: Using Vulkan's **External Semaphores** and **Fences**, you can synchronize rendering across a multi-GPU projector cluster, ensuring all GPUs swap buffers at the exact same hardware clock cycle. +* **Global Illuminant Consistency**: You can use Vulkan's **Query Pools** to sample the brightness of each projector and dynamically adjust your engine's lighting, ensuring a uniform visual environment across all walls. == Determinism is Mandatory -In a cluster, every computer must agree on the state of the world. If a particle system uses a random seed that differs between nodes, one wall will show a particle in a different place than the adjacent wall, breaking the immersion. Every "Random" or "Time-based" element must be synchronized across the network. +In a cluster, every computer must agree on the world state. If a particle system uses a random seed that differs between nodes, the immersion breaks. Every "Random" or "Time-based" element must be synchronized across the network. -To the user, this cluster behaves as a single, massive GPU. However, this only works if every node renders and displays the exact same frame at the exact same time. +TIP: For more information on CAVE and large-scale spatial systems, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#spaces[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/12_CAVE_Architecture/03_hardware_sync.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc index e2833abc9..6aa88315b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc @@ -1,61 +1,75 @@ :pp: {plus}{plus} -= Geometric Correction: Mapping 3D Scenes to Physical Surfaces += Geometric Correction & Edge Blending -The core of **Geometric Correction** is a coordinate transformation. We start with a perfectly rectangular image rendered by our engine (the "Source"), and we need to transform it into a "Warped" image that looks correct when projected onto a physical surface like a curved screen or a corner. +Geometric correction transforms a rectilinear image into a "warped" one that looks correct when projected onto physical surfaces like curved screens or corners. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While standard HMD runtimes handle warping internally, for custom projector setups or curved display walls, you must implement your own geometric correction. You use Vulkan's compute shaders and subgroup operations to implement pixel-perfect edge blending and warping that goes beyond the standard HMD-focused OpenXR model. == The Concept: Reverse Lookup To understand warping, you must think in reverse. Instead of asking "Where does this source pixel go?", we ask **"For this pixel on my wall, where should I look in my rendered frame?"** 1. **Rectilinear Source**: A standard 3D render (straight lines, 90-degree corners). -2. **Physical Destination**: A curved screen or a corner where two walls meet. +2. **Physical Destination**: A curved screen or a corner where walls meet. 3. **The Warp Map**: A Look-Up Table (LUT) that maps every destination pixel to a $(u, v)$ coordinate in the source render. -Treating correction as a reverse lookup is essential because it ensures that every pixel on the output display is filled exactly once, preventing gaps or "holes" in the final image. +The runtime maintains a persistent database of "Warp Meshes" and calculates the projective transformations (keystoning) required to "square up" the image for physical surfaces. == Implementing the Warp Map -The "brain" of this operation is the **Warp Map**. This is typically a `vk::raii::Image` containing floating-point data. +The "brain" of this operation is the **Warp Map**, typically a `vk::raii::Image` containing floating-point data. We use a compute shader to perform the reverse lookup: + +[source,cpp] +---- +// Configure the warp map texture binding using designated initializers +vk::DescriptorImageInfo warpMapInfo{ + .sampler = *pointSampler, + .imageView = *warpMapView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal +}; + +vk::WriteDescriptorSet descriptorWrite{ + .dstSet = *warpDescriptorSet, + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eSampledImage, + .pImageInfo = &warpMapInfo +}; +---- + +In Slang, the compute kernel maps destination pixels to source coordinates: [source,slang] ---- -// A simple compute-based warping kernel in Slang -RWTexture2D outputTarget; // The final image sent to the projector -Texture2D sourceRender; // The rectilinear frame we rendered -Texture2D warpMap; // The LUT containing (u, v) coordinates - -[numthreads(16, 16, 1)] -void main(uint3 dispatchThreadID : SV_DispatchThreadID) -{ - float2 outputDim; - outputTarget.GetDimensions(outputDim.x, outputDim.y); - - if (dispatchThreadID.x >= outputDim.x || dispatchThreadID.y >= outputDim.y) - return; - - // 1. Look up the source coordinate from the Warp Map - // This tells us: "To draw pixel (x,y) on the wall, look at (u,v) in the render" +// Compute-based warping kernel in Slang +void main(uint3 dispatchThreadID : SV_DispatchThreadID) { + // 1. Look up where this wall pixel should sample from the rendered image float2 sourceUV = warpMap.Load(int3(dispatchThreadID.xy, 0)); - // 2. Sample the source image using high-quality interpolation - // We use Bilinear or Bicubic filtering to prevent "aliasing" - SamplerState linearSampler; - float4 color = sourceRender.SampleLevel(linearSampler, sourceUV, 0); - - // 3. Write the warped pixel to the output - outputTarget[dispatchThreadID.xy] = color; + // 2. Sample and write to the final projector output + outputTarget[dispatchThreadID.xy] = sourceRender.SampleLevel(linearSampler, sourceUV, 0); } ---- -== Luminance Summation and Edge Blending +== Advanced: Pixel-Perfect Blending and Dynamic Keystoning + +Vulkan allows for a level of precision beyond standard calibration: -In a multi-projector setup, the edges of the images overlap. +* **Pixel-Perfect Edge Blending**: Using Vulkan's **Subgroup Operations**, you can implement real-time edge blending with pixel-perfect accuracy. This allows you to eliminate "hot spots" in overlapping projector regions dynamically. +* **Dynamic Keystoning**: By using compute shaders and tracked object data from OpenXR, you can implement dynamic keystoning that updates your warp map in real-time if a projector is physically moved during the session. -* **The Problem**: In the overlap region, you have light from *two* projectors hitting the same spot. This area will be twice as bright as the rest of the wall, creating a distracting "hot spot." -* **The Solution**: **Luminance Edge Blending**. We include an alpha channel in our Warp Map that defines a smooth gradient (1.0 to 0.0) in the overlap region. By multiplying the final color by this alpha, we "fade out" one projector as the other "fades in," maintaining uniform brightness across the entire wall. +== Luminance Summation and Edge Blending + +In a multi-projector setup, the edges overlap. This area will be twice as bright as the rest of the wall. To solve this, we include an alpha channel in our Warp Map that defines a smooth gradient. By multiplying the final color by this alpha, we maintain uniform brightness across the entire wall. == Precision: Bicubic vs Sinc Filtering -Because warping involves sampling a texture at non-integer coordinates, the quality of your filtering is paramount. While `SamplerState` with linear filtering is fast, it can lead to "blurring" in areas of high warp. High-end spatial engines often implement custom **Bicubic** or **Lanczos** filters in the compute shader to preserve sharpness during the coordinate transformation. +Because warping involves sampling a texture at non-integer coordinates, the quality of your filtering is paramount. While linear filtering is fast, it can lead to "blurring." High-end spatial engines often implement custom **Bicubic** or **Lanczos** filters in the compute shader to preserve sharpness during the coordinate transformation. + +TIP: For more information on projective geometry and warping, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#samplers[Vulkan Specification on Samplers], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc index 2b8b06cd9..81139e842 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/03_post_process_warping.adoc @@ -1,14 +1,21 @@ :pp: {plus}{plus} = Post-Process Warping and Lens Distortion -While the previous section focused on external physical surfaces like CAVE walls, **Warping** is also a fundamental part of head-mounted displays (HMDs). In a headset, the lenses themselves introduce significant **Pin-Cushion Distortion**. To counteract this, the engine must apply a **Barrel Distortion** post-process so that the final image looks rectilinear to the user. +Warping is fundamental to head-mounted displays (HMDs) to counteract the physical **Pin-Cushion Distortion** of the lenses. + +== Ecosystem Perspective + +This chapter falls under the category: **What your OpenXR Runtime is Probably Doing**. + +While we teach the underlying mathematics and Vulkan implementation of lens warping, for most commercial HMDs, these operations are handled internally by the runtime's compositor. Understanding this process is critical for developing custom hardware or implementing advanced post-warp effects that go beyond the standard runtime pipeline. == The Physics of the Lens: Why Warp? -To understand HMD warping, you must look at the glass. VR lenses are designed to take a tiny display and magnify it to cover your entire field of view. This magnification is not uniform. +VR lenses are designed to take a tiny display and magnify it to cover your entire field of view. This magnification is not uniform, introducing **Pin-Cushion Distortion**. -1. **Pin-Cushion Distortion**: The lens physically stretches the image more at the corners than in the center. -2. **The Solution**: We pre-distort the image in the opposite direction (**Barrel Distortion**). By "squeezing" the corners in our shader, the lens's physical stretch pulls them back into their correct rectilinear positions. +To counteract this, the engine must apply a **Barrel Distortion** post-process. By "squeezing" the corners in our shader, the lens's physical stretch pulls them back into their correct rectilinear positions. + +While most runtimes apply this distortion implicitly, you can implement your own for custom hardware or advanced effects like **Chromatic Aberration Correction**. == Mathematical Models of Distortion @@ -23,19 +30,43 @@ float2 ApplyDistortion(float2 uv, float2 k) float2 d = uv - center; float r2 = dot(d, d); - // Barrel distortion: k[0] and k[1] are typically positive - // This shifts pixels towards the center based on their distance + // Barrel distortion: shifts pixels towards the center based on distance float2 distortedD = d * (1.0 + k.x * r2 + k.y * r2 * r2); return center + distortedD; } ---- -== Chromatic Aberration: The Rainbow Effect +On the C++ side, we use designated initializers to bind our source renders for these post-process passes: + +[source,cpp] +---- +// Binding the source render for post-process warping using designated initializers +vk::DescriptorImageInfo sourceInfo{ + .sampler = *linearSampler, + .imageView = *sourceView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal +}; + +vk::WriteDescriptorSet write{ + .dstSet = *postProcessSet, + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eSampledImage, + .pImageInfo = &sourceInfo +}; +---- + +== Advanced: DLSS and Custom Distortion Kernels -Lenses also refract different wavelengths of light at different angles. This causes Red, Green, and Blue light to hit the user's eye at slightly different positions, creating "color fringing" or **Chromatic Aberration**. +Vulkan allow us to push the boundaries of HMD post-processing: + +* **DLSS and Super Resolution**: You can implement your own **Deep Learning Super Sampling** pass before the runtime's warp. This ensures the compositor receives a high-detail image even from a lower-resolution source, providing visual clarity beyond standard OpenXR pipelines. +* **Custom Post-Warp Effects**: Using Vulkan's **Shader Objects**, you can dynamically inject effects like localized lens flare or optical film grain directly into the final composition pass, bypassing the runtime's "black box" compositor. + +== Chromatic Aberration: The Rainbow Effect -To fix this, we apply a slightly different warp to each color channel. In our Vulkan compute shader, this is as simple as performing three samples instead of one: +Lenses also refract different wavelengths of light at different angles, creating "color fringing." To fix this, we apply a slightly different warp to each color channel. [source,slang] ---- @@ -49,8 +80,8 @@ color.a = 1.0; == Rendering 1.2x: The Oversampling Tax -Because warping "squeezes" the image, some areas of your render will be scaled down while others are scaled up. To prevent the center of your view from looking blurry after the warp, you must render your 3D scene at a **Higher Resolution** than the physical display (typically 1.2x to 1.4x the panel resolution). This ensures that even after the distortion is applied, you still have 1:1 pixel mapping in the most critical areas. +Because warping "squeezes" the image, some areas are scaled down while others are scaled up. To prevent blurring in the center of your view, you must render your 3D scene at a **Higher Resolution** than the display (typically 1.2x to 1.4x). -In the next chapter, we will move from pixels to 4D functions by exploring **LightField Theory**. +TIP: For more information on post-process warping, check out the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#distortion[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/02_geometric_correction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/13_Warp_and_Blend/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc index 36e380ff7..9557780af 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc @@ -1,46 +1,45 @@ :pp: {plus}{plus} = 4D LightField Representation -When we talk about representing a lightfield in the engine, we need a mathematical structure that maps a 4D light ray into a linear memory address. +Representing a lightfield requires mapping a 4D light ray into a linear memory address that the GPU can process efficiently. -== The 7D Plenoptic Function: The Starting Point +== Ecosystem Perspective -To understand lightfields, we must first look at the **Plenoptic Function** (from the Latin *plenus* meaning full and *opticus* meaning vision). This is a 7D function latexmath:[P(x, y, z, \theta, \phi, \lambda, t)] that describes every photon in the universe: +This chapter falls under the category: **Beyond the OpenXR Standard**. -* latexmath:[(x, y, z)]: Position in space. -* latexmath:[(\theta, \phi)]: Direction of travel. -* latexmath:[\lambda]: Wavelength (color). -* latexmath:[t]: Time. +Standard OpenXR runtimes are focused on 2D projection layers. You use Vulkan's raw buffer management and custom shader indexing to implement high-dimensional 4D lightfields, which allows for true holographic representations that go beyond the standard spatial rendering model. -== Reduction to 4D: The Light Box +== The 7D Plenoptic Function: The Starting Point -Storing 7 dimensions is impossible. To make this practical for a GPU, we make three assumptions: +To understand lightfields, we must first look at the **Plenoptic Function** (from the Latin *plenus* meaning full). This is a 7D function latexmath:[P(x, y, z, \theta, \phi, \lambda, t)] that describes every photon in the universe. -1. **Static Scene**: We ignore time (latexmath:[t]). -2. **RGB Color**: We reduce wavelength (latexmath:[\lambda]) to three channels. -3. **Empty Space**: We assume light doesn't change color as it travels through air. This means we only need to know where a ray enters and leaves a "Light Box." +Storing 7 dimensions is impossible. For a GPU, we make three assumptions: +1. **Static Scene**: We ignore time. +2. **RGB Color**: We reduce wavelength to three channels. +3. **Empty Space**: We assume light doesn't change color in air, only needing to know where a ray enters and leaves a "Light Box." -This leaves us with **4 Dimensions**: two for the entry point (**ST plane**) and two for the entry direction (**UV plane**). This is the **Two-Plane Parametrization**. +This leaves us with **4 Dimensions**: two for the entry point (**ST plane**) and two for the entry direction (**UV plane**). == Mapping to Vulkan Memory -In Vulkan, we represent this 4D structure as a massive `vk::raii::Buffer`. Because the dataset is so large, we use `RWStructuredBuffer` in Slang to handle the indexing. +In Vulkan, we represent this 4D structure as a massive `vk::raii::Buffer`. We use designated initializers to configure this high-bandwidth storage: [source,cpp] ---- -// Initializing a 4D LightField buffer using RAII +// Initializing a 4D LightField buffer using designated initializers vk::BufferCreateInfo bufferInfo{ .size = spatialRes * directionalRes * sizeof(glm::vec4), .usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, .sharingMode = vk::SharingMode::eExclusive }; - vk::raii::Buffer lightFieldBuffer(device, bufferInfo); ---- +The runtime ensures that your "Light Box" remains perfectly stationary in physical space (the Stage Space), allowing the lightfield to maintain its directional integrity even as the user's head moves. + == Indexing the 4th Dimension -In our Slang shaders, we treat this linear memory as a 4D structure. Slang's ability to handle custom indexing makes it easy to calculate the 1D index from our 4D coordinates. +In our Slang shaders, we map 4D coordinates to a linear index in this buffer: [source,slang] ---- @@ -51,8 +50,6 @@ struct LightFieldRay { RWStructuredBuffer lightFieldData; // Mapping 4D coordinates to a linear index -// st: entry point on the spatial plane -// uv: entry point on the directional plane uint getLinearIndex(uint2 st, uint2 uv, uint2 stRes, uint2 uvRes) { return st.x + st.y * stRes.x + uv.x * (stRes.x * stRes.y) + @@ -60,6 +57,13 @@ uint getLinearIndex(uint2 st, uint2 uv, uint2 stRes, uint2 uvRes) { } ---- -By using this representation, the engine can treat a lightfield as a simple array of "light fragments," making it compatible with existing spatial pipelines while providing the foundation for complex plenoptic synthesis. +== Advanced: Sparse LightField Residency + +Vulkan allows you to manage gigabyte-scale lightfields beyond standard buffer limits: + +* **Holographic Virtual Texturing**: Using Vulkan's **Sparse Resident Resources**, you can keep only the currently visible rays resident in VRAM. This allows you to swap data in and out of the GPU based on the user's predicted movement. +* **Real-Time Generation**: You can use Vulkan's **Mesh Shaders** to "bake" lightfield updates in real-time for dynamic objects, ensuring moving actors and changing lighting are captured in your plenoptic representation. + +TIP: For more information on lightfield theory and buffer management, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#resources-buffers[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc index 025931062..4b9df38c0 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/03_high_density_view_arrays.adoc @@ -1,44 +1,55 @@ :pp: {plus}{plus} = High-Density View Arrays -Managing a 4D lightfield poses a significant memory challenge. Even at modest resolutions, the total number of color samples can quickly reach gigabytes. +Managing a 4D lightfield requires optimizing both storage and access to handle datasets that can easily reach gigabytes in size. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +Standard OpenXR runtimes are focused on fixed-resolution projection layers. You use Vulkan's Buffer Device Address (BDA) and Sparse Residency to manage the high-density view arrays required for plenoptic data, allowing for spatial detail that exceeds the standard OpenXR buffer model. == The Gigabyte Problem -Consider a modest lightfield: -* **Spatial**: latexmath:[1024 \times 1024] entry points. -* **Directional**: latexmath:[32 \times 32] angles. -* **Data**: latexmath:[1024 \times 1024 \times 32 \times 32 \approx 1 \text{ billion rays}]. +Managing a 4D lightfield poses a significant memory challenge. A modest lightfield with latexmath:[1024 \times 1024] entry points and latexmath:[32 \times 32] angles contains roughly 1 billion rays. At 16 bytes per ray (RGBA FP32), that is **16 GB of VRAM**—exceeding the capacity of many consumer GPUs. -At 16 bytes per ray (RGBA FP32), that is **16 GB of VRAM**. This exceeds the capacity of many consumer GPUs. To make this work, we must optimize both storage and access. +The runtime work closely with the OS to "Pin" these massive buffers in VRAM, often using **Sparse Residency** (via `VK_KHR_sparse_binding`) to load only the visible tiles. == FP16 and Quantization: Bandwidth Savings -The first step is reducing the precision of each ray. Because lightfields often exhibit high directional redundancy (adjacent rays are very similar), we can compress the color data. - -In Slang, we can use `half4` instead of `float4`. This immediately halves the VRAM footprint to **8 GB**. +Reducing precision is the first step in optimization. Using `half4` instead of `float4` in Slang immediately halves the VRAM footprint. Adjacent rays in a lightfield are often very similar, allowing for high-quality compression without significant visual loss. == Buffer Device Address (BDA): Pointer-Based Access -With such large datasets, the overhead of standard Vulkan Descriptor Sets can become a bottleneck. By using **Buffer Device Address**, we can pass a raw 64-bit pointer to our lightfield data directly into our shaders. +With such large datasets, the overhead of standard Vulkan Descriptor Sets can become a bottleneck. By using **Buffer Device Address**, we can pass a raw 64-bit pointer to our data directly into our shaders. [source,cpp] ---- -// Using BDA to get a raw pointer to a high-density buffer +// Using BDA to get a raw pointer using designated initializers vk::BufferDeviceAddressInfo addressInfo{ .buffer = *lightFieldBuffer }; +uint64_t gpuAddress = device.getBufferAddress(addressInfo); -uint64_t bufferPointer = device.getBufferAddress(addressInfo); +// Passing the pointer to a Slang shader via Push Constants +struct PushConstants { + uint64_t lightFieldPtr; + uint2 stRes; + uint2 uvRes; +}; +commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eCompute, 0, pc); ---- -This is particularly useful for **N-View** systems where we might be accessing dozens of different view layers in a single pass. We can simply pass the pointer once and calculate offsets mathematically. +== Advanced: Neural Compression and 4D Cache Locality -== Cache Locality: Spatial-Directional Tiling +Vulkan allows for memory optimizations beyond standard linear buffer models: + +* **Neural Lightfield Compression**: Using Vulkan's **Video Decode** and **Compression** extensions, you can implement custom compute-based decompressors. This allows you to fit significantly more spatial detail into the same VRAM footprint. +* **4D Tiling and Swizzling**: You can use compute shaders to implement custom swizzling (like Morton-order indexing), keeping GPU caches efficient when sampling high-density lightfield arrays. -A standard 4D array layout is terrible for GPU caches. If the shader samples a ray and then its neighbor, the neighbor might be megabytes away in linear memory, causing a "Cache Miss." +== Cache Locality: Spatial-Directional Tiling -To fix this, we implement **4D Tiling**. We reorganize the data so that a latexmath:[4 \times 4 \times 4 \times 4] block of spatial and directional rays are stored contiguously. This ensures that when a Compute Unit (CU) fetches one sample, the neighboring rays are already in the L1/L2 cache. +A standard 4D array layout is terrible for GPU caches. To fix this, we implement **4D Tiling**, reorganizing data so that small spatial and directional blocks are contiguous. [source,slang] ---- @@ -54,8 +65,6 @@ uint getTiledIndex(uint2 st, uint2 uv) { } ---- -By combining data compression, raw pointer flexibility via BDA, and tiled memory layouts, we can manage massive lightfield datasets that would otherwise be impossible to store. - -In the next chapter, we will see how to turn these rays back into images using **Plenoptic Synthesis**. +TIP: For more information on BDA and sparse resources, check out the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-device-address[Vulkan Specification on Device Addresses], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/02_4d_lightfield_representation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/14_LightField_Theory/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc index 2a3e955a2..d4f19a0e6 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc @@ -1,23 +1,27 @@ :pp: {plus}{plus} -= Specialized Fragment Shaders for Synthesis += Plenoptic Synthesis Shaders -To turn a 4D lightfield back into a 2D image, we use a process called **Image-Based Rendering (IBR)**. Instead of drawing triangles, we treat our 4D buffer as a dense "cloud" of light rays and selectively pull the ones that would pass through our virtual camera lens. +To turn a 4D lightfield back into a 2D image, we use **Image-Based Rendering (IBR)** to selectively sample the rays that pass through our virtual camera lens. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides the exact eye poses for each frame, the complex process of reconstructing smooth wavefronts from plenoptic data via 4D quadrilinear interpolation is a Vulkan-driven synthesis technique. You use Vulkan's high-speed memory access and subgroup operations to implement these advanced spatial effects that go beyond the standard runtime projection model. == The Concept: Reconstructing the Wavefront -To understand synthesis, imagine your virtual camera is looking into the "Light Box" we defined in the previous chapter. +To turn a 4D lightfield back into a 2D image, we use **Image-Based Rendering (IBR)**. Instead of drawing triangles, we treat our 4D buffer as a dense "cloud" of light rays and selectively pull the ones that would pass through our virtual camera lens. -1. **Ray Tracing**: For every pixel on your screen, you calculate a ray starting at your eye and passing through that pixel. -2. **Intersection**: You find where that ray hits the **ST plane** (position) and the **UV plane** (direction) of your lightfield. -3. **Synthesis**: You look up the color of the ray at those coordinates in your 4D buffer. +1. **Ray Tracing**: For every pixel, we calculate a ray starting at the eye and passing through that pixel. +2. **Intersection**: We find where that ray hits the **ST plane** (position) and the **UV plane** (direction) of our lightfield. +3. **Synthesis**: We look up the color of the ray at those coordinates in our 4D buffer. -== Quadrilinear Interpolation: The 16-Sample Lookup +The runtime provides the exact `XrView` pose for each frame, which our synthesis shader uses to determine the origin and direction of the rays, ensuring the perspective is perfectly aligned with the headset. -Because our lightfield is stored as a discrete grid, the ray we calculate will rarely hit an exact sample point. We must interpolate. In 4D, this requires **Quadrilinear Interpolation**. +== Quadrilinear Interpolation: The 16-Sample Lookup -* **Bilinear on ST**: Interpolate between the 4 nearest spatial neighbors. -* **Bilinear on UV**: Interpolate between the 4 nearest directional neighbors. -* **Result**: To get a single smooth pixel, you must sample **16 different rays** (4 spatial $\times$ 4 directional). +Because our lightfield is stored as a discrete grid, we must interpolate. In 4D, this requires **Quadrilinear Interpolation**, sampling 16 different rays (4 spatial $\times$ 4 directional) for every pixel. [source,slang] ---- @@ -30,21 +34,47 @@ float4 sampleLightField(float2 st, float2 uv) { float2 uvF = frac(uv); // 2. Perform ST-plane interpolation at the surrounding UV coordinates - // fetch() uses the linear index calculation we learned in the last chapter float4 c00 = lerp(lerp(fetch(st0, uv0), fetch(st0 + uint2(1, 0), uv0), stF.x), lerp(fetch(st0, uint2(0, 1), uv0), fetch(st0 + uint2(1, 1), uv0), stF.x), stF.y); - // ... repeat for the other 3 UV neighbors (c10, c01, c11) ... + // ... repeat for the other 3 UV neighbors ... // 3. Perform the final UV-plane interpolation - return lerp(lerp(c00, c10, uvF.x), lerp(c01, c11, uvF.x), uvF.y); + return finalColor; } ---- +On the C++ side, we use designated initializers to bind the lightfield resources for the synthesis pass: + +[source,cpp] +---- +// Binding the lightfield for synthesis using designated initializers +vk::DescriptorBufferInfo lfBufferInfo{ + .buffer = *lightFieldBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE +}; + +vk::WriteDescriptorSet lfWrite{ + .dstSet = *synthesisSet, + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lfBufferInfo +}; +---- + +== Advanced: Cooperative Ray-Casting and Dynamic Occlusion + +Vulkan allow us to optimize synthesis beyond standard view-dependent fetching: + +* **Cooperative Ray-Casting**: Using Vulkan's **Subgroup Operations**, you can coordinate ray-casting across neighboring pixels, sharing the cost of the expensive 4D lookups and plane intersections. +* **Dynamic Occlusion**: You can use Vulkan's **Depth-Stencil Testing** to implement dynamic occlusion between your lightfield synthesis and traditional rasterized 3D geometry, ensuring virtual objects can realistically "sink" into the holographic scene. + == Performance: Coalescing the Rays -Because we are doing 16 lookups per pixel, we must be extremely mindful of **SIMT Coalescing**. If we are not careful with our memory layout, these 16 lookups will scatter across different DRAM pages, causing a "Memory Wall" where the GPU sits idle while waiting for data. +Because we are doing 16 lookups per pixel, we must be mindful of **SIMT Coalescing**. By using the **4D Tiling** we implemented in Chapter 14, we ensure that these 16 samples are geographically close in physical VRAM. This allows the GPU to satisfy all requests with just one or two cache line fetches, keeping our plenoptic synthesis running at high frame rates. -By using the **4D Tiling** we implemented in Chapter 14, we ensure that these 16 samples are geographically close in physical VRAM. This allows the GPU to satisfy all 16 requests with just one or two cache line fetches, keeping our plenoptic synthesis running at high frame rates. +TIP: For more information on image-based rendering and quadrilinear filtering, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-interpolation[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc index bb83e57cd..185851c7b 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/03_ray_traced_synthesis.adoc @@ -1,22 +1,44 @@ :pp: {plus}{plus} -= Ray Traced Synthesis and Microlens Arrays += Ray Traced Synthesis & Microlens Arrays -While the raster path for plenoptic synthesis is fast, it's often an approximation. For high-fidelity holographic displays, we want to simulate the exact physical optics of a display—particularly how rays travel through a **Microlens Array** (MLA). This is where **Vulkan Ray Tracing** (VRT) becomes the engine's primary tool for synthesis. +For high-fidelity holographic displays, we use **Vulkan Ray Tracing (VRT)** to simulate the exact physical optics of a **Microlens Array (MLA)**. -== Physical Optics: The Microlens Array (MLA) +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +Standard OpenXR runtimes are built around traditional projection models. You use Vulkan's Ray Tracing pipelines to simulate complex virtual microlens optics, allowing for vergence-accommodation solutions and sparse lightfield traversal that exceed the capabilities of the standard runtime compositor. -To understand ray-traced synthesis, you must understand how lightfield cameras work. They use a **Microlens Array**—a grid of thousands of tiny lenses placed in front of the sensor. Each microlens captures a small "patch" of the scene from multiple angles. +== Physical Optics: The Microlens Array (MLA) -In our ray tracing pipeline, we reverse this process: +To understand ray-traced synthesis, you must understand how lightfield cameras work. They use a **Microlens Array**—a grid of thousands of tiny lenses placed in front of a sensor. In our ray tracing pipeline, we reverse this process: 1. **Primary Ray**: We trace a ray from the virtual eye to the screen. 2. **Microlens Intersection**: We simulate the ray passing through a virtual microlens. -3. **Refraction**: We calculate how the lens bends the ray based on its physical properties (focal length, curvature). -4. **LightField Lookup**: The refracted ray now points to a specific 4D coordinate in our plenoptic buffer. +3. **Refraction**: We calculate how the lens bends the ray based on focal length and curvature. +4. **LightField Lookup**: The refracted ray now points to a specific 4D plenoptic coordinate. + +The runtime provides the focal length, pitch, and curvature parameters of the physical MLA via vendor extensions, and may even provide a pre-built acceleration structure for the lens geometry. == Implementing the Virtual Lens in Slang -Using the `RaytracingAccelerationStructure` in Vulkan, we can simulate complex lenses that a raster shader cannot. +Using the `RaytracingAccelerationStructure` in Vulkan, we can simulate complex lenses that a raster shader cannot. We use designated initializers to configure the ray generation pass: + +[source,cpp] +---- +// Configuring the ray tracing pipeline using designated initializers +vk::RayTracingPipelineCreateInfoKHR pipelineInfo{ + .pNext = &dynamicRenderingInfo, // Link to our dynamic rendering state + .stageCount = static_cast(stages.size()), + .pStages = stages.data(), + .groupCount = static_cast(groups.size()), + .pGroups = groups.data(), + .maxPipelineRayRecursionDepth = 1, + .layout = *pipelineLayout +}; +---- + +In our Slang RayGen shader, we perform the physical optics simulation: [source,slang] ---- @@ -27,10 +49,9 @@ void rayGenMain() { float2 uv = float2(pixelID) / float2(DispatchRaysDimensions().xy); // 1. Trace a ray through the virtual microlens optics - // This function calculates the refraction based on the MLA geometry RayDesc ray = generateRayThroughLens(uv); - // 2. Intersect the ray with our 4D LightField "box" (the ST/UV planes) + // 2. Intersect the ray with our 4D LightField "box" float4 color = intersectLightField(ray, lightFieldData); // 3. Write the final result to the OpenXR swapchain image @@ -38,12 +59,19 @@ void rayGenMain() { } ---- +== Advanced: Holographic Persistence and MLA Optimization + +Vulkan allows you to push synthesis beyond standard view-dependent models: + +* **Temporal Holographic Persistence**: Using Vulkan's **Ray Tracing Motion Blur** extensions, you can simulate the persistence of a holographic display by tracing rays across the predicted display window. This ensures moving objects remain sharp and stable. +* **Subgroup MLA Intersections**: You can use Vulkan's **Subgroup Operations** to implement highly efficient ray-circle or ray-hexagon intersection logic, sharing the computational cost of the lens simulation across multiple threads. + == Why Ray Trace? Physical Fidelity -Ray tracing allows us to simulate **Vergence-Accommodation Conflict (VAC)** solutions. By adjusting the virtual focal length of our microlens array in real-time, we can change the "Plane of Focus" of the holographic image. This allows the user's eyes to naturally focus on near or far virtual objects, eliminating the eye strain common in standard fixed-focus headsets. +Ray tracing allows us to simulate **Vergence-Accommodation Conflict (VAC)** solutions. By adjusting the virtual focal length of our microlens array in real-time, we can change the "Plane of Focus" of the holographic image. This allows the user's eyes to naturally focus on near or far virtual objects, eliminating eye strain. -Furthermore, ray tracing allows us to handle **Non-Uniform LightFields**. If our 4D data is stored in a sparse tree (like the Octrees we covered in the Compute tutorial), ray tracing can traverse those structures much more efficiently than a series of nested loops in a fragment shader. +Furthermore, ray tracing handles **Non-Uniform LightFields** (like octrees) much more efficiently than nested loops in a fragment shader. -By combining the 4D theory of LightFields with the physical accuracy of Vulkan Ray Tracing, we create a spatial rendering pipeline that can handle the most advanced holographic displays on the market. +TIP: For more information on ray tracing and holographic displays, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#ray-tracing[Vulkan Specification on Ray Tracing], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/02_synthesis_shaders.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/15_Plenoptic_Synthesis/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc index 79f4a194a..95a4726d5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc @@ -3,6 +3,12 @@ To make our virtual objects interact with the physical world, our engine must first capture that world's geometry. This process is known as **Environmental Ingestion** or **Scene Understanding**. Using OpenXR extensions like `XR_MSFT_scene_understanding` or the cross-vendor **Scene Mesh** functionality, we can bridge the gap between photons and triangles. +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Ingesting environmental geometry through OpenXR extensions is a standard way to enable Mixed Reality interactions. You must use these extensions to stream sensor-generated meshes into your Vulkan-mapped buffers to ensure physical and virtual alignment. + == The Concept: The Digital Twin Scene understanding turns the messy physical world into a structured **Digital Twin**. The XR runtime uses the headset's cameras and depth sensors (like **LiDAR**) to build this twin in real-time. This isn't a static model; it is a dynamic, evolving representation of the user's environment. @@ -15,29 +21,38 @@ Scene understanding turns the messy physical world into a structured **Digital T When we request a scene mesh from OpenXR, we aren't just getting a one-time blob of data. We are subscribing to a stream of updates. In our engine, we handle this by creating dynamic `vk::raii::Buffer` objects that we update whenever the runtime signals that the environment has changed. -Crucially, because this data is generated by the headset's sensors, we often receive it in specialized formats. OpenXR allows us to query these buffers directly, but we must be careful with our synchronization—using **Timeline Semaphores** to ensure the GPU doesn't try to render a spatial mesh while the XR runtime is still updating its vertices. - [source,cpp] ---- -// Querying the scene mesh from OpenXR with vulkan-hpp -XrSceneMeshBuffersMSFT meshBuffers{XR_TYPE_SCENE_MESH_BUFFERS_MSFT}; -meshBuffers.vertexCapacityInput = maxVertices; -meshBuffers.vertexBuffer = reinterpret_cast(*vertexBufferMemory); -meshBuffers.indexCapacityInput = maxIndices; -meshBuffers.indexBuffer = reinterpret_cast(*indexBufferMemory); - -// The call that populates our Vulkan-mapped memory +// Querying the scene mesh into Vulkan-mapped buffers using designated initializers +XrSceneMeshBuffersMSFT meshBuffers{ + .type = XR_TYPE_SCENE_MESH_BUFFERS_MSFT, + .vertexCapacityInput = maxVertices, + .vertices = reinterpret_cast(*vertexBufferMemory), + .indexCapacityInput = maxIndices, + .indices = reinterpret_cast(*indexBufferMemory) +}; + +// The call that populates our Vulkan-mapped memory from the sensors xrGetSceneMeshBuffersMSFT(sceneHandle, meshId, &meshBuffers); ---- +Crucially, because this data is dynamic, we must use **Timeline Semaphores** to ensure the GPU doesn't try to render a spatial mesh while the XR runtime is still updating its vertices. + == Semantic Labels: Meaningful Interaction Semantic labels are the "Intelligence Layer" of scene understanding. Instead of just seeing geometry, our engine can ask: "Give me all surfaces labeled as **FLOOR**." This allows us to implement high-level spatial logic: -* **Physics Interaction**: Virtual objects should bounce off tables but pass through "Air" (regions the sensor hasn't scanned yet). +* **Physics Interaction**: Virtual objects should bounce off tables but pass through "Air." * **AI Navigation**: Characters should only walk on surfaces labeled as "Floor." -* **Contextual Occlusion**: If a mesh is labeled as "Wall," we can prioritize it for occlusion tests, ensuring that virtual windows don't accidentally render "inside" your physical walls. +* **Contextual Occlusion**: If a mesh is labeled as "Wall," we can prioritize it for occlusion tests, ensuring virtual windows don't render "inside" physical walls. + +== Advanced: Real-Time Texture Mapping and Decimation + +Vulkan allows us to refine the environmental ingestion beyond standard meshes: + +* **Real-Time Texture Mapping**: Using Vulkan's **Video Capture** and **Sampling** extensions, you can use raw camera feeds to project physical room colors onto the scanned geometry in a compute shader, creating a photorealistic "Digital Twin." +* **Physics Primitive Decimation**: Runtimes typically provide high-poly meshes. You can use Vulkan **Compute Shaders** to perform real-time mesh decimation, simplifying the OpenXR scan into clean physics primitives (planes/boxes) for more efficient collision detection. -By ingesting these meshes directly into our Vulkan buffers, we enable real-time interaction between our shaders and the user's physical room. +TIP: For more details, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#scene_understanding[OpenXR Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc index d4e4c0a2e..40496cc1f 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/03_zero_copy_hand_off.adoc @@ -1,28 +1,32 @@ :pp: {plus}{plus} = Zero-Copy Hand-off for Sensor Data -To achieve real-time scene understanding, we cannot afford to process high-resolution sensor data on the CPU. Moving depth maps or raw camera feeds from the headset's hardware into system RAM, processing them with the CPU, and then sending them back to the GPU is simply too slow for Mixed Reality (**MR**). We need a **Zero-Copy Hand-off**. +Real-time scene understanding requires moving sensor data (like depth maps or camera feeds) directly to the GPU without CPU intervention to minimize latency. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Importing runtime-allocated sensor buffers using Vulkan's external memory extensions is a standard way to achieve high-performance Mixed Reality. You must use these extensions to enable direct memory access (DMA) between the headset's sensors and your compute shaders. == The Concept: Direct Memory Access (DMA) -In a zero-copy hand-off, the OpenXR runtime captures sensor data directly into a GPU buffer that our Vulkan engine can see. This uses **DMA** (Direct Memory Access) to bypass the host processor entirely. +In a zero-copy hand-off, the OpenXR runtime captures sensor data directly into a GPU buffer that our Vulkan engine can see, bypassing the host processor entirely via **DMA** (Direct Memory Access). -1. **VRAM Allocation**: The XR runtime or the OS driver allocates a region of **VRAM** (Video RAM) for the sensor data. -2. **External Memory Handle**: OpenXR provides a handle to this memory using extensions like `VK_KHR_external_memory`. -3. **Engine Import**: Our engine imports this handle, wrapping it in a `vk::raii::Image` or `vk::raii::Buffer`. -4. **Compute Access**: Our compute shaders (authored in **Slang**) process this raw data directly in-place. +1. **VRAM Allocation**: The runtime or OS driver allocates VRAM for the sensor data. +2. **External Memory Handle**: OpenXR provides a handle to this memory (e.g., via `VK_KHR_external_memory`). +3. **Engine Import**: Our engine imports this handle, wrapping it in a `vk::raii::DeviceMemory` object. +4. **Compute Access**: Our compute shaders process this raw data in-place. == Implementation: Piping Data to Compute -By bypassing the CPU, we reduce latency and free up cycles for the main game simulation. In our engine, we use this for **Sensor Fusion**—the process of combining depth data with color camera feeds to create high-fidelity, textured spatial meshes. - -When importing these buffers, we must ensure our Vulkan memory types match the requirements of the external handle. This typically requires matching the **LUID** (Locally Unique Identifier) of the device that created the memory. +By bypassing the CPU, sensor data moves at the speed of the GPU's internal bus, keeping **Motion-to-Photon Latency** below the threshold of human perception. we use designated initializers to configure the memory allocation and import: [source,cpp] ---- -// Importing external sensor memory into Vulkan RAII +// Importing external sensor memory using designated initializers vk::ImportMemoryBufferInfoKHR importInfo{ - .handleType = vk::ExternalMemoryHandleTypeFlagBits::eOpaqueFd, // Linux/Android (or Win32 on Windows) + .handleType = vk::ExternalMemoryHandleTypeFlagBits::eOpaqueFd, // OpaqueFd for Linux/Android .handle = sensorMemoryHandle }; @@ -36,10 +40,19 @@ vk::MemoryAllocateInfo allocInfo{ vk::raii::DeviceMemory importedMemory(device, allocInfo); ---- +The runtime coordinates the DMA transfer and may "alias" physical memory so both the tracking service and your engine can see the same frame simultaneously without duplication. + == Why Zero-Copy is Critical for MR -In Mixed Reality (MR), users are often viewing the real world through **Passthrough**—a digital video feed of their surroundings. If there is even a 20ms delay between the camera capturing the room and the GPU displaying it, the user will feel a nauseating disconnect between their physical movement and the visual feed. This is known as **Motion-to-Photon Latency**. +In Mixed Reality (MR), users view the real world through **Passthrough**. Even a 20ms delay creates a nauseating disconnect. Zero-copy hand-offs ensure that sensor data arrives with minimum latency, maintaining the illusion that virtual and physical worlds occupy the same space. + +== Advanced: Sensor Denoising and High-Frequency Injection + +Vulkan allow us to refine sensor data beyond standard streams: + +* **On-the-Fly Processing**: Using Vulkan's **Hardware Accelerators**, you can run specialized denoising or depth-completion shaders on raw sensor data, providing a cleaner "Digital Twin" than the raw OpenXR stream. +* **High-Frequency Injection**: You can use Vulkan's **Asynchronous Transfer Queues** to interpolate sensor data in parallel with your 90Hz rendering loop, ensuring virtual occluders stay perfectly aligned even if sensors report at a lower frequency (e.g., 30Hz). -Zero-copy hand-offs ensure that sensor data moves at the speed of the GPU's internal bus, keeping the latency below the threshold of human perception and maintaining the illusion that the virtual and physical worlds occupy the same space. +TIP: For more information on external memory and zero-copy transfers, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#external-memory[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/02_environmental_ingestion.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/16_Scene_Understanding/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc index f1ec2d353..94fb0e317 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc @@ -1,54 +1,64 @@ :pp: {plus}{plus} = On-GPU Inference with Cooperative Matrices -To process the flood of real-world data coming from our zero-copy sensor hand-offs, our engine needs a way to "understand" geometry and motion instantly. Standard compute shaders are excellent for general-purpose parallel tasks, but neural networks rely on heavy matrix multiplication—a task that standard **ALU** (Arithmetic Logic Unit) pipelines can find inefficient. +Neural networks rely on heavy matrix multiplication. To process sensor data at spatial frequencies, we use **Cooperative Matrices** (detailed in our Advanced Compute series) to tap into the GPU's dedicated ML hardware. -To solve this, we use **Cooperative Matrices** (as detailed in our Advanced Compute series), which tap into the GPU's dedicated hardware for high-speed matrix math. +== Ecosystem Perspective -== The Concept: Specialized ML Hardware +This chapter falls under the category: **Beyond the OpenXR Standard**. -In a spatial engine, we use ML (Machine Learning) for tasks like **Hand Tracking** and **Object Recognition**. These tasks involve multiplying massive matrices of weights against input sensor data to produce probabilities or coordinates. +Standard OpenXR runtimes perform sensor ingestion but do not mandate how that data is processed. You use Vulkan's Cooperative Matrix extensions to implement high-speed ML inference on-GPU, enabling predictive filtering and object recognition that the standard OpenXR frame loop does not natively coordinate. -* **Standard Compute**: Each thread works independently on a single scalar or vector operation. To multiply a large matrix, threads must coordinate manually via shared memory, often hitting bandwidth bottlenecks. -* **Cooperative Matrices**: A whole **Subgroup** (32 or 64 threads) works as a single unit to multiply a large matrix tile in one operation. This "cooperation" allows the hardware to bypass traditional cache levels and use high-speed internal paths (like **Tensor Cores** on NVIDIA or **Matrix Accelerators** on AMD). +== The Concept: Specialized ML Hardware -== Implementing Hand-Pose Refinement +Neural networks rely on heavy matrix multiplication. Standard compute shaders are excellent for general-purpose tasks, but for ML, we use **Cooperative Matrices** (detailed in our Advanced Compute series). This allows a whole **Subgroup** (32 or 64 threads) to work as a single unit to multiply large matrix tiles in one operation, bypassing traditional cache levels for maximum throughput: -Consider raw hand-tracking data from a headset. It is often noisy—your virtual fingers might "jitter" as the sensor loses sight of them. We can use a small neural network to **Refine** these poses, using the previous 5 frames of movement to predict the most likely "smooth" position of each joint. +[source,cpp] +---- +// Enabling cooperative matrix features using designated initializers +vk::PhysicalDeviceCooperativeMatrixFeaturesKHR coopMatrixFeatures{ + .cooperativeMatrix = VK_TRUE +}; + +vk::DeviceCreateInfo deviceCreateInfo{ + .pNext = &coMatrixFeatures +}; +---- -By performing this inference on the GPU, we can use **GEMM** (General Matrix Multiply) operations to process all joint refinements in parallel. +In Slang, we use the `cooperative_matrix` type to tap into dedicated hardware: [source,slang] ---- // Using Slang's Matrix fragments for cooperative ML inference -// Note: Requires Vulkan 1.3 with VK_KHR_cooperative_matrix [shader("compute")] -[numthreads(32, 1, 1)] // Subgroup-sized for optimal matrix math +[numthreads(32, 1, 1)] void performHandRefinement() { - // 1. Load raw joint data and neural network weights into fragments - // These matrices represent a 16x16 tile of the larger network cooperative_matrix input; cooperative_matrix weights; input.load(rawJointBuffer, jointOffset); weights.load(weightBuffer, weightOffset); - // 2. Perform high-speed Matrix-Multiply-Accumulate (MMA) - // The hardware handles the subgroup-wide synchronization automatically + // High-speed Matrix-Multiply-Accumulate (MMA) auto result = mul(input, weights); - // 3. Store the "Refined" joint positions directly back to the Action Space buffer result.store(refinedPoseBuffer, poseOffset); } ---- -== Why On-GPU? Latency and Power +== Advanced: Predictive Gaze and Multi-GPU Inference + +Vulkan allows us to push ML performance beyond standard ingestion: -Performing ML inference on the GPU is critical for spatial computing for two primary reasons: +* **Predictive Gaze Smoothing**: Using **Cooperative Matrix** and **Subgroup Operations**, you can run a small neural network directly in your frame loop to predict the user's future gaze point based on recent OpenXR telemetry, faster than the extension can report. +* **Multi-GPU Inference**: You can use Vulkan's **Device Groups** to distribute ML inference workloads (like object recognition) across multiple GPUs, aggregating results before the OpenXR composition pass. + +== Why On-GPU? Latency and Power -1. **Latency**: If we sent sensor data to the CPU for ML processing, the finger movement would visibly lag behind the user's physical hand. This breaks the feeling of **Presence**. -2. **Power Efficiency**: Matrix accelerators are significantly more energy-efficient than general-purpose ALUs for these specific tasks. For mobile headsets, this translates directly to longer sessions and reduced thermal throttling. +Performing ML inference on the GPU is critical for spatial computing: +1. **Latency**: Sending sensor data to the CPU for ML processing causes visible lag, breaking the feeling of **Presence**. +2. **Power Efficiency**: Matrix accelerators are significantly more energy-efficient than general-purpose ALUs. For mobile headsets, this reduces thermal throttling. -By integrating ML directly into our spatial compute pipeline, we turn raw, noisy sensor data into clean, interactive inputs with the same low-latency performance as our main rendering path. +TIP: For more information on cooperative matrices and ML inference, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VK_KHR_cooperative_matrix[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc index c3f3a067f..86ca767f5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/03_refining_spatial_data.adoc @@ -1,21 +1,47 @@ :pp: {plus}{plus} -= Refining Spatial Data: From Messy to Clean += Refining Spatial Data -The environmental meshes we ingested in Chapter 16 are often "noisy." They contain holes where the LiDAR sensor couldn't reach, jagged edges on curved surfaces, and floating artifacts caused by reflections or moving objects. To turn these into clean, interactive geometry, we use ML-driven refinement. +Environmental meshes ingested from sensors often contain noise and holes. We use ML-driven refinement to turn these raw scans into clean, interactive geometry. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides environmental meshes, the standard does not manage the geometric quality or topological integrity of the scan. You use Vulkan's compute shaders and ray-tracing acceleration structures to implement real-time mesh repair and mirror-aware refinement, providing a level of physical interaction that the standard OpenXR ingestion doesn't natively coordinate. == The Concept: Topology Inference -Standard mesh smoothing algorithms (like Laplacian or Gaussian smoothing) can remove noise, but they are "dumb"—they can't fill large holes or "guess" missing geometry. **Topology Inference** uses a neural network trained on millions of real-world scans to predict the missing parts of a scene. +Standard mesh smoothing removal noise but cannot fill holes or "guess" missing geometry. **Topology Inference** uses neural networks to predict missing parts of a scene. + +1. **Denoising**: Identifying and removing "geometry dust." +2. **Infilling**: Predicting curvature (e.g., a wall behind a plant) to bridge gaps. +3. **Primitive Approximation**: Suggesting low-poly primitives (like a box for a table) to replace complex, noisy meshes. -1. **Denoising**: The model identifies small floating vertices or "geometry dust" that doesn't belong to any major surface and marks them for removal. -2. **Infilling**: The model predicts the curvature of a surface (like a wall behind a plant) and generates new triangles to bridge gaps in the scan. -3. **Primitive Approximation**: For optimal performance, the model can suggest replacing a complex, high-poly "Table" mesh with a clean, low-poly box primitive that has the same physical bounds. +The runtime handles the LiDAR-to-mesh conversion and temporal smoothing (SLAM), ensuring the refined geometry stays anchored in physical space. == Implementation: ML-Aided Surface Reconstruction -In our engine, we use a compute shader to feed the raw spatial vertices into our ML model (processed via cooperative matrices). The model outputs a **Saliency Map**—a probability grid that tells us which regions are likely solid, high-confidence surfaces and which are noise. +In our engine, we use a compute shader to feed raw spatial vertices into our ML model. The model outputs a **Saliency Map**—a probability grid that guides the reconstruction. + +[source,cpp] +---- +// Binding the spatial mesh and saliency map using designated initializers +vk::DescriptorBufferInfo meshInfo{ + .buffer = *rawMeshBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE +}; + +vk::WriteDescriptorSet meshWrite{ + .dstSet = *reconstructionSet, + .dstBinding = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &meshInfo +}; +---- -We author this reconstruction logic in **Slang**, using structured buffers to manage our dynamic mesh data. +In Slang, we use the probability data to "snap" vertices to clean planes or cull artifacts: [source,slang] ---- @@ -23,29 +49,27 @@ We author this reconstruction logic in **Slang**, using structured buffers to ma [shader("compute")] [numthreads(64, 1, 1)] void reconstructSurfaces(uint3 dtid : SV_DispatchThreadID) { - // 1. Query the ML saliency map for this vertex's region + // Query the ML saliency map for this vertex's region float surfaceProbability = mlSaliencyMap.Load(dtid.xy); if (surfaceProbability > 0.95) { - // 2. High confidence: Snap the vertex to the predicted "clean" plane + // High confidence: Snap to the predicted "clean" plane float3 refinedPos = predictCleanSurface(dtid.xyz); updateMeshVertex(dtid.x, refinedPos); } else if (surfaceProbability < 0.1) { - // 3. Low confidence: This is likely a sensor artifact (e.g., a mirror reflection) - // Mark the vertex as "degenerate" to prevent it from rendering + // Low confidence: This is likely a sensor artifact (e.g., a mirror reflection) degenerateMeshVertex(dtid.x); } } ---- -== Interactive Geometry: The Final Goal +== Advanced: Real-Time Mesh Repair and Mirror Estimation -By refining the spatial data, we move from simple "Visual Passthrough" to true **Physical Interaction**. +Vulkan allows for a level of geometric cleanup beyond standard feeds: -* **Pixel-Perfect Occlusion**: A clean, refined wall mesh allows for perfect depth-aware occlusion of virtual objects, preventing them from "leaking" through the edges of physical furniture. -* **Realistic Shadows**: We can cast virtual shadows onto a refined "Table" mesh. Because the mesh is clean, the shadows look crisp and "grounded," making virtual objects feel like they truly exist in the room. -* **Robust Physics**: A refined floor mesh prevents virtual objects from falling through small "cracks" in the raw spatial scan, ensuring a stable simulation. +* **Real-Time Mesh Repair**: Using Vulkan's **Ray Tracing Acceleration Structures**, you can implement real-time mesh decimation and repair on-the-fly, filling holes that the standard OpenXR feed does not manage. +* **Reflective Surface Estimation**: Sensor-based meshes often fail on mirrors or glass. You can use Vulkan **Ray Tracing** to identify potential reflective surfaces by tracing rays against the scanned room and adjusting the OpenXR geometry to account for windows. -By using on-GPU ML to polish our spatial data, we bridge the final gap between the messy physical world and the digital precision required for high-end XR. In the next chapter, we will see how to combine these refined meshes with our rendering to achieve **Semantic Occlusion**. +TIP: For more information on scene understanding and mesh refinement, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_EXT_scene_understanding[XR_EXT_scene_understanding Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/02_on_gpu_inference.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/17_ML_Inference_Spatial/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc index 7185b3b36..05ccf5a3a 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc @@ -1,51 +1,71 @@ :pp: {plus}{plus} -= ML-Driven Segmentation: Understanding Reality += ML-Driven Segmentation -In standard Augmented Reality (AR), virtual objects are often drawn directly on top of the camera feed. This is known as **Naïve Compositing**, and it frequently leads to a jarring "Ghosting Effect"—where a virtual robot appears to float in front of a real table that should logically be occluding it. To fix this, we use **Semantic Segmentation**. +Semantic segmentation labels every pixel in the camera feed with a category, providing the pixel-perfect understanding required to blend virtual and physical content realistically. -== The Concept: Pixel-Wise Understanding +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR provides basic environment blending, the standard does not manage pixel-perfect semantic labels or edge refinement. You use Vulkan's compute shaders and subgroup operations to implement advanced temporal edge refining and occlusion logic that the standard OpenXR alpha-blending model does not natively manage. -Semantic segmentation is the process of labeling every pixel in the camera feed with a category. Unlike simple object detection, which just places a box around an item, segmentation provides a high-resolution, pixel-perfect understanding of the scene. +== The Concept: Pixel-Wise Understanding -1. **Input**: A raw RGB camera frame captured by the headset. -2. **ML Pass**: A neural network (often a **U-Net** or **Transformer** architecture) processes the frame. -3. **Output**: A **Segmentation Mask** where each pixel value corresponds to a category: 0 for Background, 1 for Table, 2 for Human Hand, etc. +Semantic segmentation labels every pixel in the camera feed with a category (e.g., 0 for Background, 1 for Table, 2 for Human Hand). This provides a pixel-perfect understanding of the scene, allowing the engine to make intelligent blending decisions. -By understanding *what* occupies every pixel in the real world, our engine can make intelligent decisions about *how* to blend virtual content, ensuring that digital objects respect the physical boundaries of the user's room. +The runtime generates these masks from the camera feed, often performing latency compensation by "warping" the mask to match the user's latest head pose before the hand-off. == Implementing the Occlusion Mask -In our Vulkan engine, we treat the output of this ML pass as a specialized texture: the **Occlusion Mask**. This texture is used in our final composite shader to gate the visibility of virtual pixels. +In our Vulkan engine, we treat the ML output as an **Occlusion Mask** texture. We use designated initializers to bind this mask in our composite pass: + +[source,cpp] +---- +// Binding the semantic mask using designated initializers +vk::DescriptorImageInfo maskInfo{ + .sampler = *pointSampler, + .imageView = *semanticMaskView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal +}; + +vk::WriteDescriptorSet maskWrite{ + .dstSet = *compositeSet, + .dstBinding = 1, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eSampledImage, + .pImageInfo = &maskInfo +}; +---- -Authoring this in **Slang**, we can efficiently sample the mask and perform conditional logic to handle complex real-world occlusion. +In Slang, we use the labels to gate the visibility of virtual pixels: [source,slang] ---- // A Slang fragment shader utilizing a semantic mask for occlusion [shader("pixel")] float4 fragmentMain(V2P input) : SV_Target { - // 1. Sample the virtual scene color float4 virtualColor = sceneTexture.Sample(input.uv); - - // 2. Sample the ML-generated semantic mask (using integer labels) uint label = semanticMask.Sample(input.uv).r; - // 3. Perform Semantic Occlusion - // If the pixel is labeled as "Human Hand", we hide the virtual object - // to allow the user's physical hand to appear "in front" of the digital content. + // If the pixel is labeled as "Hand", we hide the virtual object if (label == SEMANTIC_LABEL_HAND) { - // Transparent output allows the passthrough camera feed to show through - return float4(0, 0, 0, 0); + return float4(0, 0, 0, 0); // Transparent output } - return virtualColor; } ---- +== Advanced: Temporal Edge Refining and Atomic Depth + +Vulkan allows for a level of compositing quality beyond standard mask feeds: + +* **Temporal Edge Refining**: Using Vulkan's **Subgroup Operations**, you can analyze the segmentation mask over multiple frames to "refine" noisy edges into a smooth alpha channel, providing much higher quality than the raw OpenXR feed. +* **Depth-Aware Semantic Composite**: You can use Vulkan's **Atomic Operations** to combine OpenXR labels with your engine's depth data, resolving complex overlaps that the standard OpenXR alpha-blending model does not natively coordinate. + == The Depth Buffer of Reality -While semantic labels tell us *what* an object is, they don't always provide its exact distance. High-end spatial engines combine these labels with raw depth data from **LiDAR**. This enables **Depth-Aware Semantic Occlusion**, where a virtual robot can hide behind a real chair but still appear in front of a real-world wall further back in the room. +Semantic labels tell us *what* an object is, but combining them with **LiDAR** data enables **Depth-Aware Semantic Occlusion**. A virtual robot can hide behind a real chair while appearing in front of a real wall further back. By utilizing on-GPU inference, we generate these masks with minimal latency, ensuring the virtual world responds to physical movement instantly. -By utilizing on-GPU inference, we generate these masks with minimal latency, ensuring that as the user moves their hand, the virtual world responds and "tucks behind" it instantly. +TIP: For more information on semantic segmentation and image blending, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_FB_semantic_labels[XR_FB_semantic_labels Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc index 927ee640c..08ff9e3bf 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/03_per_pixel_masking.adoc @@ -1,22 +1,30 @@ :pp: {plus}{plus} -= Per-Pixel Masking: Stable Compositing += Per-Pixel Masking & Stable Compositing -Even with a perfect semantic mask, compositing virtual objects with the real world is physically complex. The edges where a virtual asset meets a real-world object often appear "jittery" or suffer from "haloing"—a visual artifact where the background camera feed bleeds into the virtual foreground. To solve this, we use stable **Per-Pixel Masking** and high-fidelity compositing. +Compositing virtual objects with the real world requires stable per-pixel masking to prevent "haloing" and "jitter" at the occlusion boundaries. + +== Ecosystem Perspective + +This chapter falls under the category: **Beyond the OpenXR Standard**. + +While OpenXR manages basic environment blending, the standard does not manage physically-based light transport or bloom masking. You use Vulkan's hardware stencil pipeline and compute shaders to implement advanced physically-based compositing and temporal stability that the standard OpenXR compositor does not natively coordinate. == The Concept: The Compositing Equation -In standard rendering, we use simple alpha blending to combine images. In Mixed Reality (**MR**), our "background" is the live camera feed (passthrough). To achieve realistic occlusion, we must apply our semantic mask as a **Gating Function**. +In Augmented Reality (AR), our "background" is the live camera feed (passthrough). To achieve realistic occlusion, we apply a semantic mask as a **Gating Function**. -1. **Hard Occlusion**: A binary decision (0 or 1). While simple, this leads to aliased, jagged edges that shimmer as the user's head moves. -2. **Soft Occlusion**: A smooth gradient at the boundaries. By "feathering" the mask, we can blend the virtual and real worlds naturally, hiding the slight imperfections of the ML segmentation pass. +1. **Hard Occlusion**: A binary decision (0 or 1). While simple, this leads to aliased edges that shimmer during head movement. +2. **Soft Occlusion**: A smooth gradient at the boundaries. By "feathering" the mask, we blend virtual and real worlds naturally, hiding imperfections in the ML pass. + +The runtime handles the selection of the alpha blending mode and may apply late-stage feathering to the alpha channel you provide to ensure sharpness. == Implementing Stencil-Based Masking -In our Vulkan engine, we can use the **Stencil Buffer** to perform high-speed masking. This is more efficient than performing complex branching in a fragment shader, as the hardware can discard pixels before they ever reach the shading stage. +In our Vulkan engine, we use the **Stencil Buffer** to perform high-speed masking. This is more efficient than fragment shader branching, as the hardware can discard pixels early. [source,cpp] ---- -// Configuring stencil masking with vulkan-hpp +// Configuring stencil masking using designated initializers vk::StencilOpState stencilOp{ .failOp = vk::StencilOp::eKeep, .passOp = vk::StencilOp::eReplace, @@ -24,25 +32,29 @@ vk::StencilOpState stencilOp{ .compareOp = vk::CompareOp::eAlways, .compareMask = 0xFF, .writeMask = 0xFF, - .reference = 0x1 // Mark real-world occluders with 0x1 + .reference = 0x1 // Mark real-world occluders }; -// 1. Fill the stencil buffer using the ML segmentation mask -cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, 0x1); -cmd.draw(6, 1, 0, 0); // Full-screen quad or mesh-based occluder - -// 2. Render virtual scene where stencil is NOT 0x1 -cmd.setStencilCompareMask(vk::StencilFaceFlagBits::eFrontAndBack, 0xFF); -cmd.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack, 0x1); -// CompareOp::eNotEqual ensures we only draw on virtual pixels +vk::PipelineDepthStencilStateCreateInfo depthStencil{ + .stencilTestEnable = VK_TRUE, + .front = stencilOp, + .back = stencilOp +}; ---- -== Temporal Stability: The Edge Jitter Problem +We first fill the stencil buffer using the ML segmentation mask, then render the virtual scene where the stencil is NOT 0x1. + +== Advanced: Physically-Based Compositing and Bloom Masking + +Vulkan allows us to push occlusion quality beyond standard alpha blending: -Because ML masks are generated frame-by-frame, they can slightly flicker due to camera noise or changes in lighting. This creates **Temporal Instability** at the occlusion edges. +* **Physically-Based Compositing**: Using Vulkan's **Dual Source Blending**, you can perform complex light-transport simulations, calculating how physical light should reflect off virtual surfaces in real-time. +* **Optical Bloom Masking**: You can use Vulkan **Compute Shaders** to detect bright physical light sources and apply custom "Bloom Masking" to ensure virtual occlusions feel solid even in high-contrast environments. + +== Temporal Stability: The Edge Jitter Problem -To fix this, our engine implements **Temporal Reprojection**. We take the mask from the previous frame, re-project it using the current head pose (from OpenXR's predicted display time), and blend it with the new mask. This ensures that the boundary between reality and virtuality stays rock-steady, which is essential for maintaining user immersion and preventing motion sickness. +Because ML masks are generated frame-by-frame, they can flicker. To fix this, our engine implements **Temporal Reprojection**, taking the mask from the previous frame and re-projecting it using the current head pose (from OpenXR's predicted display time). This ensures the boundary between reality and virtuality stays steady. -By combining semantic understanding with stable per-pixel masking, we create an environment where virtual objects feel physically "locked" into the user's room. +TIP: For more information on stencil masking and alpha blending, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-stencil[Vulkan Specification on Stencil Operations], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/02_ml_driven_segmentation.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/18_Semantic_Occlusion/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc index e92f4b3fb..dddad51a5 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc @@ -14,13 +14,13 @@ We use **Direct Mode**. This allows the XR runtime to take exclusive control of == HDR10 and 10-bit Color: Beyond the Screen -While mobile platforms often have excellent HDR support, desktop GPUs allow us to leverage **High Dynamic Range (HDR)** at much higher internal resolutions and bit-depths. In XR, this is essential for physical realism. A virtual sun should be thousands of times brighter than a virtual candle. Without HDR, the engine must "tone-map" these values into the same narrow range, losing the feeling of scale. +Mobile platforms often lead the way with excellent HDR support, but on high-end desktop GPUs we can leverage **High Dynamic Range (HDR)** at much higher internal resolutions and bit-depths. In XR, this is essential for physical realism. A virtual sun should be thousands of times brighter than a virtual candle. Without HDR, the engine must "tone-map" these values into the same narrow range, losing the feeling of scale. To implement this, our `VulkanContext` negotiates a 10-bit color format—such as `VK_FORMAT_A2B10G10R10_UNORM_PACK32`—with the OpenXR runtime. This provides over a billion colors, eliminating "banding" artifacts in dark spatial environments like night scenes or caves. [source,cpp] ---- -// Configuring HDR10 for high-end desktop XR with vulkan-hpp +// Configuring HDR10 for high-end desktop XR using designated initializers vk::SwapchainCreateInfoKHR createInfo{ .imageFormat = vk::Format::eA2b10g10r10UnormPack32, .imageColorSpace = vk::ColorSpaceKHR::eHdr10St2084EXT, diff --git a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc index b7a7156b0..ad3a9f14e 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/03_mobile_mastery.adoc @@ -1,33 +1,52 @@ :pp: {plus}{plus} = Mobile Mastery: Conquering the Thermal Wall -Mobile spatial computing, on standalone headsets like the Meta Quest or Vive Focus, is an exercise in extreme efficiency. Unlike a desktop PC where we have hundreds of watts of power, a mobile headset must run its entire engine, tracking system, and display on a power budget of roughly 5–10 watts. +Mobile spatial computing is an exercise in extreme efficiency. On standalone headsets, we must run the entire engine, tracking, and display on a power budget of roughly 5–10 watts. -On mobile, the spatial mindset is about **Efficiency**—minimizing every memory access and every pixel calculation to extract maximum performance from the silicon. +== Ecosystem Perspective + +This chapter falls under the category: **What your OpenXR Runtime is Probably Doing**. + +Standalone mobile runtimes manage heavy architectural lifting like thermal throttling, direct-to-display scan-out, and synthetic frame generation (SpaceWarp) behind the scenes. You use Vulkan's tile-based rendering features to align your engine with these runtime behaviors to extract maximum performance. == The Concept: Tile-Based Rendering (TBR) -Most mobile GPUs (like those from Qualcomm, Arm, or Imagination) use a **Tile-Based Rendering** architecture. Instead of rendering the whole screen at once, the GPU splits the screen into tiny tiles (e.g., 16x16 or 32x32 pixels). It then processes each tile entirely within high-speed, low-power on-chip memory (**LDS** or **SRAM**) before writing the final result back to main memory. +Most mobile GPUs (like those from Qualcomm, Arm, or Imagination) use a **Tile-Based Rendering** architecture. Instead of rendering the whole screen at once, the GPU splits the screen into tiny tiles (e.g., 16x16 pixels). It processes each tile entirely within high-speed, low-power on-chip memory (**LDS** or **SRAM**) before writing the final result back to main memory. In mobile spatial computing, **Discarding is Winning**. -1. **Load Op**: Does the tile need to be cleared (`eClear`) or loaded from VRAM (`eLoad`)? -2. **Store Op**: Does the final color need to be written back to VRAM (`eStore`) or discarded (`eDontCare`)? -3. **Local Read**: In Vulkan 1.3, we can use the **Dynamic Rendering Local Read** extension (`VK_KHR_dynamic_rendering_local_read`) to read back color or depth values within the same tile without hitting main VRAM. This is essential for mobile-focused techniques like deferred shading or custom blending, keeping the entire operation on-chip. +[source,cpp] +---- +// Configuring optimal mobile attachments using designated initializers +vk::RenderingAttachmentInfo depthAttachment{ + .imageView = *depthView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, // CRITICAL: Discard depth after use + .clearValue = vk::ClearValue{.depthStencil = {1.0f, 0}} +}; +---- -If you don't need the depth buffer for the next frame, you MUST set its store op to `eDontCare`. This prevents the GPU from wasting battery power moving megabytes of depth data across the bus, which is the most power-hungry operation on a mobile chip. +In Vulkan 1.3, we can use the **Dynamic Rendering Local Read** extension (`VK_KHR_dynamic_rendering_local_read`) to read back color or depth values within the same tile without hitting main VRAM. This is essential for deferred shading, keeping the entire operation on-chip. == Application SpaceWarp (ASW): Synthetic Frames -To maintain a smooth 72Hz or 90Hz experience on a mobile chip, we often use **Application SpaceWarp**. This is a hardware-accelerated technique that allows the engine to render at half-framerate (e.g., 36 FPS) while the XR runtime generates every second frame synthetically. +To maintain a smooth 90Hz experience on mobile, we often use **Application SpaceWarp**. This allows the engine to render at half-framerate (e.g., 45 FPS) while the XR runtime generates every second frame synthetically. + +* **How it works**: The engine provides **Motion Vectors** and a depth map to the runtime. +* **The Result**: The GPU only performs heavy shading for half the frames, allowing for more complex scenes and higher fidelity. + +== Advanced: Tile-Local Deferred Shading and Thread Affinity + +Vulkan allow us to overcome the mobile "Thermal Wall" beyond standard loops: -* **How it works**: The engine provides **Motion Vectors** and a depth map to the runtime. These tell the compositor how each pixel moved since the last frame. -* **The Result**: The GPU only performs heavy shading work for half the frames, allowing for significantly more complex scenes and higher-fidelity spatial logic. +* **Tile-Local Deferred Shading**: Using Vulkan's **Input Attachments**, you can perform complex lighting and post-processing entirely within high-speed on-chip memory, avoiding the expensive "Main Memory Tax." +* **Real-Time Thread Affinity**: You can use Vulkan's **Priority Queues** to explicitly request "Real-Time" priority for your engine's command submission, ensuring spatial rendering is never delayed by background system updates—a level of coordination the standard does not guarantee. == Efficient Spatial Sync -Mobile headsets use **Unified Memory**, where the CPU and GPU share the same physical RAM. While this simplifies zero-copy hand-offs, it also means they are competing for the same **Memory Bandwidth**. +Mobile headsets use **Unified Memory**, where the CPU and GPU share the same physical RAM. While this simplifies zero-copy hand-offs, it also means they compete for **Memory Bandwidth**. Every texture sample and buffer write must be justified. -To master mobile spatial computing, our engine must be highly surgical with its memory access. Every texture sample and every buffer write must be justified. By focusing on tile-local memory and synthetic frame generation, we can deliver high-quality spatial experiences that run for hours on a single battery charge. +TIP: For more information on mobile optimization and tiling, consult the official link:https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#renderpass-load-store-ops[Vulkan Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/02_desktop_high_end.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/19_Platform_Divergence/04_incorporating_into_the_engine.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc index 06902d434..ff55893be 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc @@ -1,39 +1,53 @@ :pp: {plus}{plus} = Spatial Debugging: Peering into the Black Box -Debugging a spatial engine is fundamentally different from a standard 2D application. Because the XR runtime and its internal compositor are often black boxes, it can be hard to determine if a rendering error is in your engine, the OpenXR handshake, or the compositor's own reprojection logic. To solve this, we use **Spatial Debugging**. +Debugging a spatial engine is fundamentally different from a standard 2D application. Because the XR runtime and its internal compositor are often black boxes, it can be hard to determine if a rendering error is in your engine, the OpenXR handshake, or the compositor's own reprojection logic. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Instrumenting your spatial engine with debug labels and utilizing OpenXR's diagnostic extensions is a standard requirement for professional development. You must use these tools to create a unified view of your application's behavior within the runtime's ecosystem. == The Concept: Object Tagging -The **XR_EXT_debug_utils** extension allows you to "label" your OpenXR resources. This is functionally identical to Vulkan's `VK_EXT_debug_utils`. By assigning names to your **Action Sets**, **Spaces**, and **Swapchains**, you can see these names in external tools rather than just raw memory addresses. +The **XR_EXT_debug_utils** extension allows you to "label" your OpenXR resources, functionally identical to Vulkan's `VK_EXT_debug_utils`. By assigning names to your **Action Sets**, **Spaces**, and **Swapchains**, you can see these names in external tools like RenderDoc. [source,cpp] ---- -// Tagging an OpenXR swapchain for easier debugging -XrDebugUtilsObjectNameInfoEXT nameInfo{XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT}; -nameInfo.objectType = XR_OBJECT_TYPE_SWAPCHAIN; -nameInfo.objectHandle = reinterpret_cast(xrSwapchain); -nameInfo.objectName = "Main World Spatial Swapchain"; +// Tagging an OpenXR swapchain using designated initializers +XrDebugUtilsObjectNameInfoEXT nameInfo{ + .type = XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, + .objectType = XR_OBJECT_TYPE_SWAPCHAIN, + .objectHandle = reinterpret_cast(xrSwapchain), + .objectName = "Main World Spatial Swapchain" +}; // Now, tools like RenderDoc will display this name for the resource xrSetDebugUtilsObjectNameEXT(instance, &nameInfo); ---- +The runtime often activates internal validation layers when debugging is enabled, ensuring parameters like `predictedDisplayTime` are within valid ranges. + == RenderDoc for Spatial Engines -**RenderDoc** is the gold standard for Vulkan debugging, and it provides excellent support for modern spatial pipelines. +**RenderDoc** is the gold standard for Vulkan debugging, providing excellent support for spatial pipelines: -1. **Multi-View Inspection**: You can inspect each layer of your multiview swapchain individually. If one eye is black but the other is rendering, RenderDoc can show you the state of each `SV_ViewID` invocation. -2. **Resource State Inspection**: You can verify that your `vk::RenderingInfo` has the correct `viewMask` and that your **Synchronization 2** barriers are actually waiting for the correct stages. -3. **Compositor Insights**: Some advanced headsets allow RenderDoc to capture the compositor's final warped and blended frame, letting you see exactly how your layers are being manipulated. +1. **Multi-View Inspection**: You can inspect each layer of your multiview swapchain individually to find why one eye might be rendering incorrectly. +2. **Resource State Inspection**: You can verify that your `vk::RenderingInfo` has the correct `viewMask` and that your **Synchronization 2** barriers are waiting for the correct stages. +3. **Compositor Insights**: Some advanced headsets allow RenderDoc to capture the final warped and blended frame, letting you see exactly how your layers are being manipulated. -== Frame Analysis: Finding the Stutter +== Advanced: Inside-the-Shader Debugging and Sync Validation + +Vulkan allows for a level of diagnostic depth beyond standard validation: -In spatial computing, a "Bug" is often not a crash, but a **Dropped Frame**. A single dropped frame can cause a perceptible "stutter" that breaks immersion. +* **Inside-the-Shader Debugging**: Using Vulkan's **Debug Printf**, you can output real-time data directly from your shaders—such as calculated FOV tangents—providing visibility into the spatial pipeline that OpenXR layers do not offer. +* **Tracking Cross-API Race Conditions**: You can use Vulkan's **Synchronization Validation** to detect precisely when a swapchain image is being accessed by both engine and compositor simultaneously, helping debug flickering or judder. + +== Frame Analysis: Finding the Stutter -* **GPU Profiling**: Use tools like **NVIDIA Nsight Graphics** or **Radeon GPU Profiler** to see if your **Late Latching** wait is taking too long. -* **OpenXR Trace**: Most runtimes provide a tracing tool (like the Meta Quest Metrics Tool or SteamVR System Report) that shows **Compositor Pacing**. If your "App GPU Time" is low but the "Compositor Frame Missed" count is high, you likely have a synchronization race condition in your **Wait-Acquire-Release** loop. +A single dropped frame can cause a "stutter" that breaks immersion. Use profiling tools to see if your **Late Latching** wait is taking too long. Most runtimes provide tracing tools that show **Compositor Pacing**, helping you identify if your GPU time is low but frame misses are high due to synchronization race conditions. -By instrumenting your code with debug labels and utilizing frame analysis tools, you can ensure your spatial experience remains rock-steady and comfortable for the user. +TIP: For more information on debugging and validation, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_EXT_debug_utils[XR_EXT_debug_utils Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/01_introduction.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc[Next] diff --git a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc index c502087c0..9b90fafa9 100644 --- a/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc +++ b/en/OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/03_automated_qa.adoc @@ -1,36 +1,46 @@ :pp: {plus}{plus} -= Automated QA: Headless CI/CD for Spatial Engines += Automated QA & Headless CI/CD -Testing a spatial engine is traditionally a manual, time-consuming process. Someone must put on a headset, walk around the physical room, and manually check for rendering artifacts. For professional engine development, this is a major bottleneck. We need **Automated QA** that can run in a **Headless** CI/CD (Continuous Integration / Continuous Deployment) environment. +Testing a spatial engine can be automated using **Headless Runtimes** and AI-driven visual analysis, allowing for robust CI/CD pipelines without physical headsets. + +== Ecosystem Perspective + +This chapter falls under the category: **Using Vulkan with an OpenXR Runtime**. + +Integrating your spatial engine into an automated CI/CD pipeline using the `XR_EXT_headless` extension is a standard requirement for maintaining code quality at scale. You must use these extensions to create display-less sessions that can be managed by high-speed build systems. == The Concept: The Headless Runtime -A **Headless Runtime** (such as the **OpenXR Simulator** or **Null Runtime**) allows our engine to run without a physical headset or even a physical GPU (using software Vulkan implementations like **SwiftShader**). +A **Headless Runtime** (such as the OpenXR Simulator) allows our engine to run without a physical headset or GPU. -1. **Mock Tracking Data**: The simulator provides pre-recorded or scripted tracking data (poses and controller inputs). -2. **Swapchain Capture**: The engine renders frames into the OpenXR swapchain as usual, but the simulator captures these frames directly to disk rather than displaying them. -3. **Visual Validation**: We compare these captured frames against "Gold Standard" baseline images to ensure no rendering regressions have been introduced by a code change. +1. **Mock Tracking Data**: The simulator provides pre-recorded or scripted poses and controller inputs. +2. **Swapchain Capture**: The engine renders into the swapchain, and the simulator captures these frames directly to disk. +3. **Visual Validation**: We compare these frames against "Gold Standard" baselines to ensure no regressions. + +The runtime generates perfectly deterministic poses in this mode, ensuring every CI run is identical and facilitating the use of diagnostic layers to check for threading or handle issues. == AI Vision Analysis for Regressions -Traditional "Pixel-Perfect" image comparison often fails in spatial computing due to subtle differences in reprojection, slight floating-point variations in view matrices, or hardware-specific MSAA patterns. +Traditional "Pixel-Perfect" comparison often fails in spatial computing due to subtle variations in reprojection or MSAA. Instead, we use **AI Vision Models** to detect specific artifacts: -Instead, we use **AI Vision Models**. We feed the rendered frames from our CI/CD pipeline into a neural network trained to detect specific **Spatial Artifacts**: +* **Asymmetric Frustum Errors**: Detecting "stretching" or "pinching" at the view edges. +* **Multiview Inconsistencies**: Checking for objects missing from one eye. +* **Temporal Mask Flicker**: Identifying instability in **Semantic Occlusion** masks. -* **Asymmetric Frustum Errors**: Detecting if the projection matrices are causing "stretching" or "pinching" at the edges of the view. -* **Multiview Inconsistencies**: Checking if an object is missing from one eye but present in the other. -* **Temporal Mask Flicker**: Analyzing a sequence of frames to identify instability in **Semantic Occlusion** masks. +== Performance Monitoring in CI -By using AI, we can automate the "someone put on the headset" step, catching bugs within minutes of a pull request being submitted. +We track **Spatial Performance Metrics** in every build to prevent performance "creep": +* **Submission Latency**: Total time between `xrWaitFrame` and `xrEndFrame`. +* **Wait-to-Begin Gap**: CPU time spent waiting for late-latching sync. +* **Memory Footprint**: VRAM usage of 4D **LightField** and **Semantic Occlusion** buffers. -== Performance Monitoring in CI +== Advanced: Automated Visual Testing and Lens Simulation -Finally, we track **Spatial Performance Metrics** in every build to prevent performance "creep." +Vulkan allows for automated QA depth beyond standard headless execution: -* **Submission Latency**: The total time between `xrWaitFrame` and `xrEndFrame`. -* **Wait-to-Begin Gap**: The time the CPU spends waiting for the late-latching sync point. -* **Memory Footprint**: Tracking the VRAM usage of our 4D **LightField** and **Semantic Occlusion** buffers. +* **Automated Visual Regression**: Using Vulkan's **Offscreen Rendering** and **Image Readback**, you can capture every frame and compare it against golden images using SSIM (Structural Similarity Index), ensuring visual correctness without human intervention. +* **Physical Lens Simulation**: You can use Vulkan **Compute Shaders** to simulate lens artifacts (God Rays, chromatic aberration) in your CI pipeline, testing how UI and content will appear through physical glass before deploying to hardware. -By combining headless simulation, AI-driven visual analysis, and automated performance tracking, we ensure that our spatial engine remains visually stunning, stable, and efficient across every commit and every hardware target. +TIP: For more information on headless testing and simulators, consult the official link:https://registry.khronos.org/OpenXR/specs/1.1/html/xpspec.html#XR_EXT_headless[XR_EXT_headless Specification], the link:https://docs.vulkan.org/guide/latest/index.html[Vulkan Guide], and our link:00_introduction.adoc[main tutorial series]. xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/02_spatial_debugging.adoc[Previous] | xref:OpenXR_Vulkan_Spatial_Computing/20_Spatial_Diagnostics_CI_CD/04_incorporating_into_the_engine.adoc[Next] From 92adab63c8db77cb97bf3d3297155b0249f332ab Mon Sep 17 00:00:00 2001 From: swinston Date: Thu, 11 Jun 2026 18:53:29 -0700 Subject: [PATCH 13/23] Update cache key to include imgui file hash in GitHub Actions workflow --- .github/workflows/openxr_engine_ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/openxr_engine_ci.yml b/.github/workflows/openxr_engine_ci.yml index f118382fa..c44d87e4f 100644 --- a/.github/workflows/openxr_engine_ci.yml +++ b/.github/workflows/openxr_engine_ci.yml @@ -66,16 +66,16 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ccache - key: ${{ runner.os }}-openxr-ccache-${{ github.sha }} - restore-keys: ${{ runner.os }}-openxr-ccache- + key: ${{ runner.os }}-openxr-ccache-${{ hashFiles('attachments/simple_engine/imgui/**') }}-${{ github.sha }} + restore-keys: ${{ runner.os }}-openxr-ccache-${{ hashFiles('attachments/simple_engine/imgui/**') }}- - name: sccache (Windows) if: runner.os == 'Windows' uses: actions/cache@v4 with: path: ${{ env.SCCACHE_DIR }} - key: ${{ runner.os }}-openxr-sccache-${{ github.sha }} - restore-keys: ${{ runner.os }}-openxr-sccache- + key: ${{ runner.os }}-openxr-sccache-${{ hashFiles('attachments/simple_engine/imgui/**') }}-${{ github.sha }} + restore-keys: ${{ runner.os }}-openxr-sccache-${{ hashFiles('attachments/simple_engine/imgui/**') }}- - name: Cache Vulkan SDK (Windows) if: runner.os == 'Windows' From 9a10a980b5059177305faa1e23edb351211645c2 Mon Sep 17 00:00:00 2001 From: swinston Date: Fri, 12 Jun 2026 02:03:20 -0700 Subject: [PATCH 14/23] Fix the build by updating imgui dependencies and adding missing tables and widgets files. --- attachments/openxr_engine/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index f8f2388de..c47e88b53 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -151,6 +151,8 @@ set(SOURCES_COMMON ${SIMPLE_ENGINE_DIR}/imgui_system.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui_draw.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui_tables.cpp + ${SIMPLE_ENGINE_DIR}/imgui/imgui_widgets.cpp ${SIMPLE_ENGINE_DIR}/vulkan_device.cpp ${SIMPLE_ENGINE_DIR}/pipeline.cpp ${SIMPLE_ENGINE_DIR}/descriptor_manager.cpp From 693d42a5f8e53f2b1592ae1f0a0e63104b5309f7 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 28 Jun 2026 12:11:37 -0700 Subject: [PATCH 15/23] Fix for OpenXR crashes. Monado works locally. --- attachments/openxr_engine/CMakeLists.txt | 2 +- attachments/openxr_engine/renderer.h | 17 +- attachments/openxr_engine/renderer_core.cpp | 76 +- attachments/openxr_engine/renderer_utils.cpp | 348 + attachments/openxr_engine/xr_context.h | 6 + attachments/simple_engine/imgui/imconfig.h | 138 +- attachments/simple_engine/imgui/imgui.cpp | 27099 +++++++++------- attachments/simple_engine/imgui/imgui.h | 5476 ++-- .../simple_engine/imgui/imgui_draw.cpp | 9009 +++-- .../simple_engine/imgui/imgui_internal.h | 4925 ++- 10 files changed, 26659 insertions(+), 20437 deletions(-) create mode 100644 attachments/openxr_engine/renderer_utils.cpp diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index c47e88b53..87c290a3c 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -136,7 +136,7 @@ set(SOURCES_COMMON ${SIMPLE_ENGINE_DIR}/platform.cpp renderer_pipelines.cpp ${SIMPLE_ENGINE_DIR}/renderer_compute.cpp - ${SIMPLE_ENGINE_DIR}/renderer_utils.cpp + renderer_utils.cpp # OpenXR version (VK_NULL_HANDLE guard for XR device selection) ${SIMPLE_ENGINE_DIR}/renderer_ray_query.cpp memory_pool.cpp ${SIMPLE_ENGINE_DIR}/resource_manager.cpp diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index ab4763939..b5a1b6e4a 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -979,9 +979,6 @@ class Renderer { XrContext& GetXrContext() { return xrContext; } private: - XrContext xrContext; - bool xrMode = false; - // Platform Platform* platform = nullptr; @@ -1034,9 +1031,6 @@ class Renderer { vk::Format swapChainImageFormat = vk::Format::eUndefined; vk::Extent2D swapChainExtent = {0, 0}; std::vector swapChainImageViews; - // OpenXR Swapchains - std::vector eyeSwapchainImages[2]; - std::vector eyeSwapchainImageViews[2]; // Tracked layouts for swapchain images (VVL requires correct oldLayout in barriers). // Initialized at swapchain creation and updated as we transition. @@ -1119,7 +1113,6 @@ class Renderer { // Depth buffer vk::raii::Image depthImage = nullptr; std::unique_ptr depthImageAllocation = nullptr; - vk::raii::DeviceMemory depthImageMemory = nullptr; vk::raii::ImageView depthImageView = nullptr; // Forward+ configuration @@ -1925,7 +1918,15 @@ class Renderer { // Serialize descriptor writes vs command buffer recording to avoid mid-record updates during recording std::mutex renderRecordMutex; - // (Descriptor API wrappers were considered but avoided here to keep RAII types intact.) + // OpenXR-specific members placed here (after all shared members) so that simple_engine/renderer.h + // and openxr_engine/renderer.h share identical byte offsets for all common members. + // renderer_utils.cpp and other shared source files are compiled against simple_engine/renderer.h + // (source-file-dir include precedence), so the shared-member layout MUST match. + XrContext xrContext; + bool xrMode = false; + std::vector eyeSwapchainImages[2]; + std::vector eyeSwapchainImageViews[2]; + vk::raii::DeviceMemory depthImageMemory = nullptr; // Upload perf getters public: diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index c362744f7..92c4a1127 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -154,16 +154,30 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } + // In XR mode pick the device BEFORE creating the surface. The OpenXR runtime + // identifies the correct GPU via LUID/UUID, so no surface is needed for selection. + // Creating the GLFW surface first causes the XR runtime's Vulkan layer to intercept + // vkCreateXxxSurfaceKHR before the validation layer sees it, producing an untracked + // surface handle that crashes vkGetPhysicalDeviceSurfaceSupportKHR. + if (xrMode) { + if (!pickPhysicalDevice()) { + std::cerr << "Failed to pick physical device" << std::endl; + return false; + } + } + // Create surface if (!createSurface()) { std::cerr << "Failed to create surface" << std::endl; return false; } - // Pick the physical device - if (!pickPhysicalDevice()) { - std::cerr << "Failed to pick physical device" << std::endl; - return false; + // Pick the physical device (non-XR path — surface is available for present-support queries) + if (!xrMode) { + if (!pickPhysicalDevice()) { + std::cerr << "Failed to pick physical device" << std::endl; + return false; + } } // Create logical device @@ -627,9 +641,33 @@ bool Renderer::createInstance(const std::string& appName, bool enableValidationL createInfo.pNext = &validationFeatures; } - // Create instance - instance = vk::raii::Instance(context, createInfo); + // In XR mode, use xrCreateVulkanInstanceKHR (XR_KHR_vulkan_enable2) so the runtime + // injects its Vulkan layer before GLFW creates the surface. Without this, Meta's + // OpenXR layer on Windows is not in the dispatch chain at surface-creation time, + // causing vkGetPhysicalDeviceSurfaceSupportKHR to report the handle as invalid. + if (xrMode) { + PFN_xrCreateVulkanInstanceKHR pfnXrCreateVulkanInstance = nullptr; + xrGetInstanceProcAddr(xrContext.getXrInstance(), "xrCreateVulkanInstanceKHR", + reinterpret_cast(&pfnXrCreateVulkanInstance)); + if (pfnXrCreateVulkanInstance) { + XrVulkanInstanceCreateInfoKHR xrVkCreateInfo{XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR}; + xrVkCreateInfo.systemId = xrContext.getSystemId(); + xrVkCreateInfo.pfnGetInstanceProcAddr = vkGetInstanceProcAddr; + xrVkCreateInfo.vulkanCreateInfo = reinterpret_cast(&createInfo); + xrVkCreateInfo.vulkanAllocator = nullptr; + VkInstance vkInst = VK_NULL_HANDLE; + VkResult vkResult = VK_SUCCESS; + if (pfnXrCreateVulkanInstance(xrContext.getXrInstance(), &xrVkCreateInfo, &vkInst, &vkResult) == XR_SUCCESS + && vkResult == VK_SUCCESS) { + instance = vk::raii::Instance(context, vkInst); + xrContext.setVulkanInstance(*instance); + return true; + } + std::cerr << "Warning: xrCreateVulkanInstanceKHR failed, falling back to standard instance creation" << std::endl; + } + } + instance = vk::raii::Instance(context, createInfo); if (xrMode) { xrContext.setVulkanInstance(*instance); } @@ -732,8 +770,14 @@ bool Renderer::pickPhysicalDevice() { continue; } - // Check queue families + // Check queue families. In XR mode the surface may not exist yet (it is created after + // device selection), so findQueueFamilies will skip the present-support query and leave + // presentFamily unset. Fall back to the graphics family — modern discrete GPUs always + // support present on the graphics queue. QueueFamilyIndices indices = findQueueFamilies(_device); + if (!indices.presentFamily.has_value() && indices.graphicsFamily.has_value()) { + indices.presentFamily = indices.graphicsFamily; + } bool supportsGraphics = indices.isComplete(); if (!supportsGraphics) { std::cout << " - Missing required queue families" << std::endl; @@ -747,12 +791,15 @@ bool Renderer::pickPhysicalDevice() { continue; } - // Check swap chain support - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(_device); - bool swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); - if (!swapChainAdequate) { - std::cout << " - Inadequate swap chain support" << std::endl; - continue; + // Check swap chain support — skip in XR mode because the surface does not exist yet + // at the time of device selection. Swap-chain adequacy is verified at swapchain creation. + if (!xrMode) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(_device); + bool swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) { + std::cout << " - Inadequate swap chain support" << std::endl; + continue; + } } // Check for required features @@ -802,6 +849,9 @@ bool Renderer::pickPhysicalDevice() { // Store queue family indices for the selected device queueFamilyIndices = findQueueFamilies(physicalDevice); + if (!queueFamilyIndices.presentFamily.has_value() && queueFamilyIndices.graphicsFamily.has_value()) { + queueFamilyIndices.presentFamily = queueFamilyIndices.graphicsFamily; + } // Add supported optional extensions addSupportedOptionalExtensions(); diff --git a/attachments/openxr_engine/renderer_utils.cpp b/attachments/openxr_engine/renderer_utils.cpp new file mode 100644 index 000000000..d16f16f74 --- /dev/null +++ b/attachments/openxr_engine/renderer_utils.cpp @@ -0,0 +1,348 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "renderer.h" +#include +#include +#include +#include +#include + +// This file contains utility methods from the Renderer class + +// Find memory type +uint32_t Renderer::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + try { + // Get memory properties + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + // Find suitable memory type + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); + } catch (const std::exception& e) { + std::cerr << "Failed to find memory type: " << e.what() << std::endl; + throw; + } +} + +// Find supported format +vk::Format Renderer::findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { + try { + for (vk::Format format : candidates) { + vk::FormatProperties props = physicalDevice.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { + return format; + } else if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format"); + } catch (const std::exception& e) { + std::cerr << "Failed to find supported format: " << e.what() << std::endl; + throw; + } +} + +// Find depth format +vk::Format Renderer::findDepthFormat() { + try { + vk::Format depthFormat = findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment); + std::cout << "Found depth format: " << static_cast(depthFormat) << std::endl; + return depthFormat; + } catch (const std::exception& e) { + std::cerr << "Failed to find supported depth format, falling back to D32_SFLOAT: " << e.what() << std::endl; + // Fallback to D32_SFLOAT which is widely supported + return vk::Format::eD32Sfloat; + } +} + +// Check if format has stencil component +bool Renderer::hasStencilComponent(vk::Format format) { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; +} + +// Read file +std::vector Renderer::readFile(const std::string& filename) { + try { + // Open file at end to get size + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + // Get file size + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); + + // Go back to beginning of file and read data + file.seekg(0); + file.read(buffer.data(), fileSize); + + // Close file + file.close(); + + return buffer; + } catch (const std::exception& e) { + std::cerr << "Failed to read file: " << e.what() << std::endl; + throw; + } +} + +// Create shader module +vk::raii::ShaderModule Renderer::createShaderModule(const std::vector& code) { + try { + // Create shader module + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data()) + }; + + return vk::raii::ShaderModule(device, createInfo); + } catch (const std::exception& e) { + std::cerr << "Failed to create shader module: " << e.what() << std::endl; + throw; + } +} + +// Find queue families +QueueFamilyIndices Renderer::findQueueFamilies(const vk::raii::PhysicalDevice& device) { + QueueFamilyIndices indices; + + // Get queue family properties + std::vector queueFamilies = device.getQueueFamilyProperties(); + + // Find queue families that support graphics, compute, present, and (optionally) a dedicated transfer queue + for (uint32_t i = 0; i < queueFamilies.size(); i++) { + const auto& qf = queueFamilies[i]; + // Check for graphics support + if ((qf.queueFlags & vk::QueueFlagBits::eGraphics) && !indices.graphicsFamily.has_value()) { + indices.graphicsFamily = i; + } + // Check for compute support + if ((qf.queueFlags & vk::QueueFlagBits::eCompute) && !indices.computeFamily.has_value()) { + indices.computeFamily = i; + } + // In XR mode the surface is created after device selection, so the surface handle may be + // VK_NULL_HANDLE here. Guard the present-support query to avoid passing an invalid handle + // to the driver (which crashes when the XR runtime intercepts surface creation at a layer + // that sits outside the validation layer's view). + if (!indices.presentFamily.has_value() && *surface != VK_NULL_HANDLE && + device.getSurfaceSupportKHR(i, *surface)) { + indices.presentFamily = i; + } + // Prefer a dedicated transfer queue (transfer bit set, but NOT graphics) if available + if ((qf.queueFlags & vk::QueueFlagBits::eTransfer) && !(qf.queueFlags & vk::QueueFlagBits::eGraphics)) { + if (!indices.transferFamily.has_value()) { + indices.transferFamily = i; + } + } + // If all required queue families are found, we can still continue to try find a dedicated transfer queue + if (indices.isComplete() && indices.transferFamily.has_value()) { + // Found everything including dedicated transfer + break; + } + } + + // Fallback: if no dedicated transfer queue, reuse graphics queue for transfer + if (!indices.transferFamily.has_value() && indices.graphicsFamily.has_value()) { + indices.transferFamily = indices.graphicsFamily; + } + + return indices; +} + +// Query swap chain support +SwapChainSupportDetails Renderer::querySwapChainSupport(const vk::raii::PhysicalDevice& device) { + SwapChainSupportDetails details; + + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(*surface); + + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(*surface); + + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(*surface); + + return details; +} + +// Check device extension support +bool Renderer::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { + auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties(); + + // Check if all required extensions are supported + std::set requiredExtensionsSet(requiredDeviceExtensions.begin(), requiredDeviceExtensions.end()); + + for (const auto& extension : availableDeviceExtensions) { + requiredExtensionsSet.erase(extension.extensionName); + } + + // Print missing required extensions + if (!requiredExtensionsSet.empty()) { + std::cout << "Missing required extensions:" << std::endl; + for (const auto& extension : requiredExtensionsSet) { + std::cout << " " << extension << std::endl; + } + return false; + } + + return true; +} + +// Check if device is suitable +bool Renderer::isDeviceSuitable(vk::raii::PhysicalDevice& device) { + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + + // Check device extensions + bool extensionsSupported = checkDeviceExtensionSupport(device); + + // Check swap chain support + bool swapChainAdequate = false; + if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportsRequiredFeatures; +} + +// Choose swap surface format +vk::SurfaceFormatKHR Renderer::chooseSwapSurfaceFormat(const std::vector& availableFormats) { + // Look for SRGB format + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + + // If not found, return first available format + return availableFormats[0]; +} + +// Choose swap present mode +vk::PresentModeKHR Renderer::chooseSwapPresentMode(const std::vector& availablePresentModes) { + // Look for mailbox mode (triple buffering) + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + + // If not found, return FIFO mode (guaranteed to be available) + return vk::PresentModeKHR::eFifo; +} + +// Choose swap extent +vk::Extent2D Renderer::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + // Get framebuffer size + int width, height; + platform->GetWindowSize(&width, &height); + + // Create extent + vk::Extent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + // Clamp to min/max extent + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} + +// Wait for device to be idle +void Renderer::WaitIdle() { + // 1. Wait for all in-flight fences safely first + std::vector allFences; + allFences.reserve(inFlightFences.size()); + for (const auto& fence : inFlightFences) { + if (*fence) { + allFences.push_back(*fence); + } + } + if (!allFences.empty()) { + (void) waitForFencesSafe(allFences, VK_TRUE); + } + + // 2. Also wait for uploads timeline semaphore if it exists + if (*uploadsTimeline) { + uint64_t target = uploadTimelineLastSubmitted.load(std::memory_order_relaxed); + while (true) { + vk::SemaphoreWaitInfo waitInfo{}; + waitInfo.semaphoreCount = 1; + waitInfo.pSemaphores = &*uploadsTimeline; + waitInfo.pValues = ⌖ + + vk::Result r = device.waitSemaphores(waitInfo, 100'000'000ULL); // 100ms + if (r == vk::Result::eSuccess) + break; + if (r == vk::Result::eTimeout) { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + continue; + } + break; // Other error + } + } + + // 3. Final blocking wait to ensure absolute idle + // External synchronization: ensure no queue submits/presents overlap a full device idle. + // This is required for VVL cleanliness when other threads may hold or use queues. + std::lock_guard lock(queueMutex); + device.waitIdle(); +} + +vk::Result Renderer::waitForFencesSafe(const std::vector& fences, vk::Bool32 waitAll, uint64_t timeoutNs) { + if (fences.empty()) + return vk::Result::eSuccess; + + while (true) { + vk::Result r = device.waitForFences(fences, waitAll, timeoutNs); + if (r == vk::Result::eSuccess) + return vk::Result::eSuccess; + if (r == vk::Result::eTimeout) { + // Kick watchdog while we wait + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + continue; + } + return r; + } +} + +vk::Result Renderer::waitForFencesSafe(vk::Fence fence, vk::Bool32 waitAll, uint64_t timeoutNs) { + return waitForFencesSafe(std::vector{fence}, waitAll, timeoutNs); +} diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h index 4d744ab5f..921fce2d6 100644 --- a/attachments/openxr_engine/xr_context.h +++ b/attachments/openxr_engine/xr_context.h @@ -2,7 +2,11 @@ #include #include +#if defined(_WIN32) +#define XR_USE_PLATFORM_WIN32 +#elif defined(__linux__) #define XR_USE_PLATFORM_XLIB +#endif #define XR_USE_GRAPHICS_API_VULKAN #include #include @@ -28,6 +32,8 @@ class XrContext { bool createInstance(const std::string& appName); void setVulkanInstance(vk::Instance instance) { vkInstance = instance; } + XrInstance getXrInstance() const { return instance; } + XrSystemId getSystemId() const { return systemId; } bool createSession(vk::PhysicalDevice physicalDevice, vk::Device device, uint32_t queueFamilyIndex, uint32_t queueIndex); void cleanup(); diff --git a/attachments/simple_engine/imgui/imconfig.h b/attachments/simple_engine/imgui/imconfig.h index f2850ff36..b8d55842b 100644 --- a/attachments/simple_engine/imgui/imconfig.h +++ b/attachments/simple_engine/imgui/imconfig.h @@ -1,50 +1,136 @@ //----------------------------------------------------------------------------- -// USER IMPLEMENTATION -// This file contains compile-time options for ImGui. -// Other options (memory allocation overrides, callbacks, etc.) can be set at runtime via the ImGuiIO structure - ImGui::GetIO(). +// DEAR IMGUI COMPILE-TIME OPTIONS +// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. +// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. +//----------------------------------------------------------------------------- +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. +// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using. //----------------------------------------------------------------------------- #pragma once //---- Define assertion handler. Defaults to calling assert(). -// #define IM_ASSERT(_EXPR) MyAssert(_EXPR) +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export +//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import +//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden + +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names. +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87+ disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This is automatically done by IMGUI_DISABLE_OBSOLETE_FUNCTIONS. + +//---- Disable all of Dear ImGui or don't implement standard windows/tools. +// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty. + +//---- Don't implement some functions to reduce linkage requirements. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. +//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Enable Test Engine / Automation features. +//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details. -//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows. -// #define IMGUI_API __declspec( dllexport ) -// #define IMGUI_API __declspec( dllimport ) +//---- Include imgui_user.h at the end of imgui.h as a convenience +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. +//#define IMGUI_INCLUDE_IMGUI_USER_H +//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" -//---- Include imgui_user.h at the end of imgui.h -// #define IMGUI_INCLUDE_IMGUI_USER_H +//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//#define IMGUI_USE_BGRA_PACKED_COLOR -//---- Don't implement default handlers for Windows (so as not to link with OpenClipboard() and others Win32 functions) -// #define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS -// #define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS +//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 -//---- Don't implement help and test window functionality (ShowUserGuide()/ShowStyleEditor()/ShowTestWindow() methods will be empty) -// #define IMGUI_DISABLE_TEST_WINDOWS +//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined. +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined. -//---- Don't define obsolete functions names -// #define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. +//#define IMGUI_USE_STB_SPRINTF -//---- Implement STB libraries in a namespace to avoid conflicts -// #define IMGUI_STB_NAMESPACE ImGuiStb +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +//#define IMGUI_ENABLE_FREETYPE -//---- Define constructor and implicit cast operators to convert back<>forth from your math types and ImVec2/ImVec4. +//---- Use FreeType+lunasvg library to render OpenType SVG fonts (SVGinOT) +// Requires lunasvg headers to be available in the include path + program to be linked with the lunasvg library (not provided). +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// (implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE + +//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. +// This will be inlined as part of ImVec2 and ImVec4 class declarations. /* -#define IM_VEC2_CLASS_EXTRA \ - ImVec2(const MyVec2& f) { x = f.x; y = f.y; } \ +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \ operator MyVec2() const { return MyVec2(x,y); } -#define IM_VEC4_CLASS_EXTRA \ - ImVec4(const MyVec4& f) { x = f.x; y = f.y; z = f.z; w = f.w; } \ +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \ operator MyVec4() const { return MyVec4(x,y,z,w); } */ +//---- ...Or use Dear ImGui's own very basic math operators. +//#define IMGUI_DEFINE_MATH_OPERATORS + +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. +// Read about ImGuiBackendFlags_RendererHasVtxOffset for details. +//#define ImDrawIdx unsigned int + +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) +//struct ImDrawList; +//struct ImDrawCmd; +//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); +//#define ImDrawCallback MyImDrawCallback + +//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase) +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) +//#define IM_DEBUG_BREAK IM_ASSERT(0) +//#define IM_DEBUG_BREAK __debugbreak() + +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID -//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. -//---- e.g. create variants of the ImGui::Value() helper for your low-level math types, or your own widgets/helpers. +//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files) /* namespace ImGui { - void Value(const char* prefix, const MyMatrix44& v, const char* float_format = NULL); + void MyFunction(const char* name, MyMatrix44* mtx); } */ diff --git a/attachments/simple_engine/imgui/imgui.cpp b/attachments/simple_engine/imgui/imgui.cpp index f01336af5..ac76ddd19 100644 --- a/attachments/simple_engine/imgui/imgui.cpp +++ b/attachments/simple_engine/imgui/imgui.cpp @@ -1,185 +1,388 @@ -// dear imgui, v1.60 WIP +// dear imgui, v1.91.1 // (main code and documentation) -// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. -// Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. -// Get latest version at https://github.com/ocornut/imgui -// Releases change-log at https://github.com/ocornut/imgui/releases -// Gallery (please post your screenshots/video there!): https://github.com/ocornut/imgui/issues/1269 +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. + +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues/7503 (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Copyright (c) 2014-2024 Omar Cornut // Developed by Omar Cornut and every direct or indirect contributors to the GitHub. -// This library is free but I need your support to sustain development and maintenance. -// If you work for a company, please consider financial support, see Readme. For individuals: https://www.patreon.com/imgui +// See LICENSE.txt for copyright and licensing details (standard MIT License). +// This library is free but needs your support to sustain development and maintenance. +// Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts. +// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Funding +// Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + +// It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. +// Note that 'ImGui::' being a namespace, you can add functions into the namespace from your own source files, without +// modifying imgui.h or imgui.cpp. You may include imgui_internal.h to access internal data structures, but it doesn't +// come with any guarantee of forward compatibility. Discussing your changes on the GitHub Issue Tracker may lead you +// to a better solution or official support for them. /* - Index - - MISSION STATEMENT - - END-USER GUIDE - - PROGRAMMER GUIDE (read me!) - - Read first - - How to update to a newer version of Dear ImGui - - Getting started with integrating Dear ImGui in your code/engine - - Using gamepad/keyboard navigation [BETA] - - API BREAKING CHANGES (read me when you update!) - - ISSUES & TODO LIST - - FREQUENTLY ASKED QUESTIONS (FAQ), TIPS - - How can I help? - - How can I display an image? What is ImTextureID, how does it works? - - How can I have multiple widgets with the same label? Can I have widget without a label? (Yes). A primer on labels and the ID stack. - - How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? - - How can I load a different font than the default? - - How can I easily use icons in my application? - - How can I load multiple fonts? - - How can I display and input non-latin characters such as Chinese, Japanese, Korean, Cyrillic? - - How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) - - How can I use the drawing facilities without an ImGui window? (using ImDrawList API) - - I integrated Dear ImGui in my engine and the text or lines are blurry.. - - I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. - - ISSUES & TODO-LIST - - CODE +Index of this file: + +DOCUMENTATION + +- MISSION STATEMENT +- CONTROLS GUIDE +- PROGRAMMER GUIDE + - READ FIRST + - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + - HOW A SIMPLE APPLICATION MAY LOOK LIKE + - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE +- API BREAKING CHANGES (read me when you update!) +- FREQUENTLY ASKED QUESTIONS (FAQ) + - Read all answers online: https://www.dearimgui.com/faq, or in docs/FAQ.md (with a Markdown viewer) + +CODE +(search for "[SECTION]" in the code to find them) + +// [SECTION] INCLUDES +// [SECTION] FORWARD DECLARATIONS +// [SECTION] CONTEXT AND MEMORY ALLOCATORS +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +// [SECTION] MISC HELPERS/UTILITIES (File functions) +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// [SECTION] ImGuiStorage +// [SECTION] ImGuiTextFilter +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex +// [SECTION] ImGuiListClipper +// [SECTION] STYLING +// [SECTION] RENDER HELPERS +// [SECTION] INITIALIZATION, SHUTDOWN +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] ID STACK +// [SECTION] INPUTS +// [SECTION] ERROR CHECKING +// [SECTION] ITEM SUBMISSION +// [SECTION] LAYOUT +// [SECTION] SCROLLING +// [SECTION] TOOLTIPS +// [SECTION] POPUPS +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +// [SECTION] DRAG AND DROP +// [SECTION] LOGGING/CAPTURING +// [SECTION] SETTINGS +// [SECTION] LOCALIZATION +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +// [SECTION] PLATFORM DEPENDENT HELPERS +// [SECTION] METRICS/DEBUGGER WINDOW +// [SECTION] DEBUG LOG WINDOW +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) +*/ + +//----------------------------------------------------------------------------- +// DOCUMENTATION +//----------------------------------------------------------------------------- + +/* MISSION STATEMENT ================= - - Easy to use to create code-driven and data-driven tools - - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools - - Easy to hack and improve - - Minimize screen real-estate usage - - Minimize setup and maintenance - - Minimize state storage on user side - - Portable, minimize dependencies, run on target (consoles, phones, etc.) - - Efficient runtime and memory consumption (NB- we do allocate when "growing" content e.g. creating a window, opening a tree node - for the first time, etc. but a typical frame won't allocate anything) + - Easy to use to create code-driven and data-driven tools. + - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools. + - Easy to hack and improve. + - Minimize setup and maintenance. + - Minimize state storage on user side. + - Minimize state synchronization. + - Portable, minimize dependencies, run on target (consoles, phones, etc.). + - Efficient runtime and memory consumption. - Designed for developers and content-creators, not the typical end-user! Some of the weaknesses includes: - - Doesn't look fancy, doesn't animate - - Limited layout features, intricate layouts are typically crafted in code + Designed primarily for developers and content-creators, not the typical end-user! + Some of the current weaknesses (which we aim to address in the future) includes: + - Doesn't look fancy. + - Limited layout features, intricate layouts are typically crafted in code. - END-USER GUIDE + + CONTROLS GUIDE ============== - - Double-click on title bar to collapse window. - - Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin(). - - Click and drag on lower right corner to resize window (double-click to auto fit window to its contents). - - Click and drag on any empty space to move window. - - TAB/SHIFT+TAB to cycle through keyboard editable fields. - - CTRL+Click on a slider or drag box to input value as text. - - Use mouse wheel to scroll. - - Text editor: - - Hold SHIFT or use mouse to select text. - - CTRL+Left/Right to word jump. - - CTRL+Shift+Left/Right to select words. - - CTRL+A our Double-Click to select all. - - CTRL+X,CTRL+C,CTRL+V to use OS clipboard/ - - CTRL+Z,CTRL+Y to undo/redo. - - ESCAPE to revert text to its original value. - - You can apply arithmetic operators +,*,/ on numerical values. Use +- to subtract (because - would set a negative value!) - - Controls are automatically adjusted for OSX to match standard OSX text editing operations. - - Gamepad navigation: see suggested mappings in imgui.h ImGuiNavInput_ + - MOUSE CONTROLS + - Mouse wheel: Scroll vertically. + - SHIFT+Mouse wheel: Scroll horizontally. + - Click [X]: Close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click ^, Double-Click title: Collapse window. + - Drag on corner/border: Resize window (double-click to auto fit window to its contents). + - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true). + - Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack). + + - TEXT EDITOR + - Hold SHIFT or Drag Mouse: Select text. + - CTRL+Left/Right: Word jump. + - CTRL+Shift+Left/Right: Select words. + - CTRL+A or Double-Click: Select All. + - CTRL+X, CTRL+C, CTRL+V: Use OS clipboard. + - CTRL+Z, CTRL+Y: Undo, Redo. + - ESCAPE: Revert text to its original value. + - On OSX, controls are automatically adjusted to match standard OSX text editing 2ts and behaviors. + + - KEYBOARD CONTROLS + - Basic: + - Tab, SHIFT+Tab Cycle through text editable fields. + - CTRL+Tab, CTRL+Shift+Tab Cycle through windows. + - CTRL+Click Input text into a Slider or Drag widget. + - Extended features with `io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard`: + - Tab, SHIFT+Tab: Cycle through every items. + - Arrow keys Move through items using directional navigation. Tweak value. + - Arrow keys + Alt, Shift Tweak slower, tweak faster (when using arrow keys). + - Enter Activate item (prefer text input when possible). + - Space Activate item (prefer tweaking with arrows when possible). + - Escape Deactivate item, leave child window, close popup. + - Page Up, Page Down Previous page, next page. + - Home, End Scroll to top, scroll to bottom. + - Alt Toggle between scrolling layer and menu layer. + - CTRL+Tab then Ctrl+Arrows Move window. Hold SHIFT to resize instead of moving. + - Output when ImGuiConfigFlags_NavEnableKeyboard set, + - io.WantCaptureKeyboard flag is set when keyboard is claimed. + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (usually goes to back false when mouse is used). + + - GAMEPAD CONTROLS + - Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. + - Particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse! + - Download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets + - Backend support: backend needs to: + - Set 'io.BackendFlags |= ImGuiBackendFlags_HasGamepad' + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys. + - For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly. + Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.). + - BEFORE 1.87, BACKENDS USED TO WRITE TO io.NavInputs[]. This is now obsolete. Please call io functions instead! + - If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing, + with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved. + + - REMOTE INPUTS SHARING & MOUSE EMULATION + - PS4/PS5 users: Consider emulating a mouse cursor with DualShock touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + run examples/libs/synergy/uSynergy.c (on your console/tablet/phone app) + in order to share your PC mouse/keyboard. + - See https://github.com/ocornut/imgui/wiki/Useful-Extensions#remoting for other remoting solutions. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiConfigFlags_NavEnableSetMousePos flag. + Enabling ImGuiConfigFlags_NavEnableSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs Dear ImGui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved. + When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that. + (If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, Dear ImGui will misbehave as it will see your mouse moving back & forth!) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) PROGRAMMER GUIDE ================ READ FIRST - - - Read the FAQ below this section! - - Your code creates the UI, if your code doesn't run the UI is gone! == very dynamic UI, no construction/destructions steps, less data retention - on your side, no state duplication, less sync, less bugs. + ---------- + - Remember to check the wonderful Wiki (https://github.com/ocornut/imgui/wiki) + - Your code creates the UI every frame of your application loop, if your code doesn't run the UI is gone! + The UI can be highly dynamic, there are no construction or destruction steps, less superfluous + data retention on your side, less state duplication, less state synchronization, fewer bugs. - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. - - You can learn about immediate-mode gui principles at http://www.johno.se/book/imgui.html or watch http://mollyrocket.com/861 + Or browse https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html for interactive web version. + - The library is designed to be built from sources. Avoid pre-compiled binaries and packaged versions. See imconfig.h to configure your build. + - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori). + You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki. + - Dear ImGui is a "single pass" rasterizing implementation of the IMGUI paradigm, aimed at ease of use and high-performances. + For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI, + where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches. + - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. + - Please make sure you have asserts enabled (IM_ASSERT redirects to assert() by default, but can be redirected). + If you get an assert, read the messages and comments around the assert. + - This codebase aims to be highly optimized: + - A typical idle frame should never call malloc/free. + - We rely on a maximum of constant-time or O(N) algorithms. Limiting searches/scans as much as possible. + - We put particular energy in making sure performances are decent with typical "Debug" build settings as well. + Which mean we tend to avoid over-relying on "zero-cost abstraction" as they aren't zero-cost at all. + - This codebase aims to be both highly opinionated and highly flexible: + - This code works because of the things it choose to solve or not solve. + - C++: this is a pragmatic C-ish codebase: we don't use fancy C++ features, we don't include C++ headers, + and ImGui:: is a namespace. We rarely use member functions (and when we did, I am mostly regretting it now). + This is to increase compatibility, increase maintainability and facilitate use from other languages. + - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. + See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. + We can can optionally export math operators for ImVec2/ImVec4 using IMGUI_DEFINE_MATH_OPERATORS, which we use internally. + - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction + (so don't use ImVector in your code or at our own risk!). + - Building: We don't use nor mandate a build system for the main library. + This is in an effort to ensure that it works in the real world aka with any esoteric build setup. + This is also because providing a build system for the main library would be of little-value. + The build problems are almost never coming from the main library but from specific backends. - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI - - Overwrite all the sources files except for imconfig.h (if you have made modification to your copy of imconfig.h) + HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + ---------------------------------------------- + - Update submodule or copy/overwrite every file. + - About imconfig.h: + - You may modify your copy of imconfig.h, in this case don't overwrite it. + - or you may locally branch to modify imconfig.h and merge/rebase latest. + - or you may '#define IMGUI_USER_CONFIG "my_config_file.h"' globally from your build system to + specify a custom path for your imconfig.h file and instead not have to modify the default one. + + - Overwrite all the sources files except for imconfig.h (if you have modified your copy of imconfig.h) + - Or maintain your own branch where you have imconfig.h modified as a top-most commit which you can regularly rebase over "master". + - You can also use '#define IMGUI_USER_CONFIG "my_config_file.h" to redirect configuration to your own file. - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. - If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed from the public API. - If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. - Please report any issue to the GitHub page! - - Try to keep your copy of dear imgui reasonably up to date. + If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed + from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will + likely be a comment about it. Please report any issue to the GitHub page! + - To find out usage of old API, you can add '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in your configuration file. + - Try to keep your copy of Dear ImGui reasonably up to date! - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE - - Add the Dear ImGui source files to your projects, using your preferred build system. - It is recommended you build the .cpp files as part of your project and not as a library. - - You can later customize the imconfig.h file to tweak some compilation time behavior, such as integrating imgui types with your own maths types. - - See examples/ folder for standalone sample applications. - - You may be able to grab and copy a ready made imgui_impl_*** file from the examples/. + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + --------------------------------------------------------------- + - See https://github.com/ocornut/imgui/wiki/Getting-Started. + - Run and study the examples and demo in imgui_demo.cpp to get acquainted with the library. + - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder. + - Add the Dear ImGui source files + selected backend source files to your projects or using your preferred build system. + It is recommended you build and statically link the .cpp files as part of your project and NOT as a shared library (DLL). + - You can later customize the imconfig.h file to tweak some compile-time behavior, such as integrating Dear ImGui types with your own maths types. - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. + - Dear ImGui never touches or knows about your GPU state. The only function that knows about GPU is the draw function that you provide. + Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" + phases of your own application. All rendering information is stored into command-lists that you will retrieve after calling ImGui::Render(). + - Refer to the backends and demo applications in the examples/ folder for instruction on how to setup your code. + - If you are running over a standard OS with a common graphics API, you should be able to use unmodified imgui_impl_*** files from the examples/ folder. + + + HOW A SIMPLE APPLICATION MAY LOOK LIKE + -------------------------------------- + EXHIBIT 1: USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder). + The sub-folders in examples/ contain examples applications following this structure. + + // Application init: create a dear imgui context, setup some options, load fonts + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. + + // Initialize helper Platform and Renderer backends (here we are using imgui_impl_win32.cpp and imgui_impl_dx11.cpp) + ImGui_ImplWin32_Init(hwnd); + ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); + + // Application main loop + while (true) + { + // Feed inputs to dear imgui, start new frame + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + // Any application code here + ImGui::Text("Hello, world!"); + + // Render dear imgui into screen + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + g_pSwapChain->Present(1, 0); + } + + // Shutdown + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); - - Init: retrieve the ImGuiIO structure with ImGui::GetIO() and fill the fields marked 'Settings': at minimum you need to set io.DisplaySize - (application resolution). Later on you will fill your keyboard mapping, clipboard handlers, and other advanced features but for a basic - integration you don't need to worry about it all. - - Init: call io.Fonts->GetTexDataAsRGBA32(...), it will build the font atlas texture, then load the texture pixels into graphics memory. - - Every frame: - - In your main loop as early a possible, fill the IO fields marked 'Input' (e.g. mouse position, buttons, keyboard info, etc.) - - Call ImGui::NewFrame() to begin the frame - - You can use any ImGui function you want between NewFrame() and Render() - - Call ImGui::Render() as late as you can to end the frame and finalize render data. it will call your io.RenderDrawListFn handler. - (Even if you don't render, call Render() and ignore the callback, or call EndFrame() instead. Otherwhise some features will break) - - All rendering information are stored into command-lists until ImGui::Render() is called. - - Dear ImGui never touches or knows about your GPU state. the only function that knows about GPU is the RenderDrawListFn handler that you provide. - - Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" phases - of your own application. - - Refer to the examples applications in the examples/ folder for instruction on how to setup your code. - - A minimal application skeleton may be: - - // Application init + EXHIBIT 2: IMPLEMENTING CUSTOM BACKEND / CUSTOM ENGINE + + // Application init: create a dear imgui context, setup some options, load fonts ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); - io.DisplaySize.x = 1920.0f; - io.DisplaySize.y = 1280.0f; - // TODO: Fill others settings of the io structure later. + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. - // Load texture atlas (there is a default font so you don't need to care about choosing a font yet) - unsigned char* pixels; + // Build and load the texture atlas into a texture + // (In the examples/ app this is usually done within the ImGui_ImplXXX_Init() function from one of the demo Renderer) int width, height; - io.Fonts->GetTexDataAsRGBA32(pixels, &width, &height); - // TODO: At this points you've got the texture data and you need to upload that your your graphic system: - MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA) - // TODO: Store your texture pointer/identifier (whatever your engine uses) in 'io.Fonts->TexID'. This will be passed back to your via the renderer. - io.Fonts->TexID = (void*)texture; + unsigned char* pixels = nullptr; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + // At this point you've got the texture data and you need to upload that to your graphic system: + // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'. + // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ for details about ImTextureID. + MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA32) + io.Fonts->SetTexID((void*)texture); // Application main loop while (true) { - // Setup low-level inputs (e.g. on Win32, GetKeyboardState(), or write to those fields from your Windows message loop handlers, etc.) - ImGuiIO& io = ImGui::GetIO(); - io.DeltaTime = 1.0f/60.0f; - io.MousePos = mouse_pos; - io.MouseDown[0] = mouse_button_0; - io.MouseDown[1] = mouse_button_1; + // Setup low-level inputs, e.g. on Win32: calling GetKeyboardState(), or write to those fields from your Windows message handlers, etc. + // (In the examples/ app this is usually done within the ImGui_ImplXXX_NewFrame() function from one of the demo Platform Backends) + io.DeltaTime = 1.0f/60.0f; // set the time elapsed since the previous frame (in seconds) + io.DisplaySize.x = 1920.0f; // set the current display width + io.DisplaySize.y = 1280.0f; // set the current display height here + io.AddMousePosEvent(mouse_x, mouse_y); // update mouse position + io.AddMouseButtonEvent(0, mouse_b[0]); // update mouse button states + io.AddMouseButtonEvent(1, mouse_b[1]); // update mouse button states // Call NewFrame(), after this point you can use ImGui::* functions anytime + // (So you want to try calling NewFrame() as early as you can in your main loop to be able to use Dear ImGui everywhere) ImGui::NewFrame(); // Most of your application code here - MyGameUpdate(); // may use any ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); - MyGameRender(); // may use any ImGui functions as well! + ImGui::Text("Hello, world!"); + MyGameUpdate(); // may use any Dear ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); + MyGameRender(); // may use any Dear ImGui functions as well! - // Render & swap video buffers + // Render dear imgui, swap buffers + // (You want to try calling EndFrame/Render as late as you can, to be able to use Dear ImGui in your own game rendering code) + ImGui::EndFrame(); ImGui::Render(); - MyImGuiRenderFunction(ImGui::GetDrawData()); + ImDrawData* draw_data = ImGui::GetDrawData(); + MyImGuiRenderFunction(draw_data); SwapBuffers(); } // Shutdown ImGui::DestroyContext(); + To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application, + you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this. - - A minimal render function skeleton may be: - void void MyRenderFunction(ImDrawData* draw_data) + HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE + --------------------------------------------- + The backends in impl_impl_XXX.cpp files contain many working implementations of a rendering function. + + void MyImGuiRenderFunction(ImDrawData* draw_data) { // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled - // TODO: Setup viewport, orthographic projection matrix + // TODO: Setup texture sampling state: sample with bilinear filtering (NOT point/nearest filtering). Use 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines;' to allow point/nearest filtering. + // TODO: Setup viewport covering draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize + // TODO: Setup orthographic projection matrix cover draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + ImVec2 clip_off = draw_data->DisplayPos; for (int n = 0; n < draw_data->CmdListsCount; n++) { - const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by ImGui - const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by ImGui + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by Dear ImGui + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by Dear ImGui for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; @@ -189,73 +392,402 @@ } else { - // The texture for the draw call is specified by pcmd->TextureId. - // The vast majority of draw calls with use the imgui texture atlas, which value you have set yourself during initialization. - MyEngineBindTexture(pcmd->TextureId); - - // We are using scissoring to clip some objects. All low-level graphics API supports it. - // If your engine doesn't support scissoring yet, you will get some small glitches (some elements outside their bounds) which you can fix later. - MyEngineScissor((int)pcmd->ClipRect.x, (int)pcmd->ClipRect.y, (int)(pcmd->ClipRect.z - pcmd->ClipRect.x), (int)(pcmd->ClipRect.w - pcmd->ClipRect.y)); + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); + ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // We are using scissoring to clip some objects. All low-level graphics API should support it. + // - If your engine doesn't support scissoring yet, you may ignore this at first. You will get some small glitches + // (some elements visible outside their bounds) but you can fix that once everything else works! + // - Clipping coordinates are provided in imgui coordinates space: + // - For a given viewport, draw_data->DisplayPos == viewport->Pos and draw_data->DisplaySize == viewport->Size + // - In a single viewport application, draw_data->DisplayPos == (0,0) and draw_data->DisplaySize == io.DisplaySize, but always use GetMainViewport()->Pos/Size instead of hardcoding those values. + // - In the interest of supporting multi-viewport applications (see 'docking' branch on github), + // always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space. + // - Note that pcmd->ClipRect contains Min+Max bounds. Some graphics API may use Min+Max, other may use Min+Size (size being Max-Min) + MyEngineSetScissor(clip_min.x, clip_min.y, clip_max.x, clip_max.y); + + // The texture for the draw call is specified by pcmd->GetTexID(). + // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture((MyTexture*)pcmd->GetTexID()); // Render 'pcmd->ElemCount/3' indexed triangles. - // By default the indices ImDrawIdx are 16-bits, you can change them to 32-bits if your engine doesn't support 16-bits indices. - MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer, vtx_buffer); + // By default the indices ImDrawIdx are 16-bit, you can change them to 32-bit in imconfig.h if your engine doesn't support 16-bit indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset, vtx_buffer, pcmd->VtxOffset); } - idx_buffer += pcmd->ElemCount; } } } - - The examples/ folders contains many functional implementation of the pseudo-code above. - - When calling NewFrame(), the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'io.WantTextInput' flags are updated. - They tell you if ImGui intends to use your inputs. So for example, if 'io.WantCaptureMouse' is set you would typically want to hide - mouse inputs from the rest of your application. Read the FAQ below for more information about those flags. - - USING GAMEPAD/KEYBOARD NAVIGATION [BETA] - - - Ask questions and report issues at https://github.com/ocornut/imgui/issues/787 - - The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. - - Keyboard: - - Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. - - When keyboard navigation is active (io.NavActive + NavFlags_EnableKeyboard), the io.WantCaptureKeyboard flag will be set. - For more advanced uses, you may want to read from: - - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. - - io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used). - - or query focus information with e.g. IsWindowFocused(), IsItemFocused() etc. functions. - Please reach out if you think the game vs navigation input sharing could be improved. - - Gamepad: - - Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). - - See 'enum ImGuiNavInput_' in imgui.h for a description of inputs. For each entry of io.NavInputs[], set the following values: - 0.0f= not held. 1.0f= fully held. Pass intermediate 0.0f..1.0f values for analog triggers/sticks. - - We uses a simple >0.0f test for activation testing, and won't attempt to test for a dead-zone. - Your code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, maybe a power curve, etc.). - - If you need to share inputs between your game and the imgui parts, the easiest approach is to go all-or-nothing, with a buttons combo to toggle the target. - Please reach out if you think the game vs navigation input sharing could be improved. - - Mouse: - - PS4 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. - - Consoles/Tablet/Phone users: Consider using Synergy host (on your computer) + uSynergy.c (in your console/tablet/phone app) to use your PC mouse/keyboard. - - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiNavFlags_MoveMouse flag in io.NavFlags. - Enabling ImGuiNavFlags_MoveMouse instructs dear imgui to move your mouse cursor along with navigation movements. - When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantMoveMouse' to notify you that it wants the mouse cursor to be moved. - When that happens your back-end NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the binding in examples/ do that. - (If you set the ImGuiNavFlags_MoveMouse flag but don't honor 'io.WantMoveMouse' properly, imgui will misbehave as it will see your mouse as moving back and forth.) - (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want - to set a boolean to ignore your other external mouse positions until the external source is moved again.) - API BREAKING CHANGES ==================== - Occasionally introducing changes that are breaking the API. The breakage are generally minor and easy to fix. - Here is a change-log of API breaking changes, if you are using one of the functions listed, expect to have to fix some code. - Also read releases logs https://github.com/ocornut/imgui/releases for more details. - + Occasionally introducing changes that are breaking the API. We try to make the breakage minor and easy to fix. + Below is a change-log of API breaking changes only. If you are using one of the functions listed, expect to have to fix some code. + When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. + You can read releases logs https://github.com/ocornut/imgui/releases for more details. + + - 2024/08/23 (1.91.1) - renamed ImGuiChildFlags_Border to ImGuiChildFlags_Borders for consistency. kept inline redirection flag. + - 2024/08/22 (1.91.1) - moved some functions from ImGuiIO to ImGuiPlatformIO structure: + - io.GetClipboardTextFn -> platform_io.Platform_GetClipboardTextFn + changed 'void* user_data' to 'ImGuiContext* ctx'. Pull your user data from platform_io.ClipboardUserData. + - io.SetClipboardTextFn -> platform_io.Platform_SetClipboardTextFn + same as above line. + - io.PlatformOpenInShellFn -> platform_io.Platform_OpenInShellFn (#7660) + - io.PlatformSetImeDataFn -> platform_io.Platform_SetImeDataFn + - io.PlatformLocaleDecimalPoint -> platform_io.Platform_LocaleDecimalPoint (#7389, #6719, #2278) + - access those via GetPlatformIO() instead of GetIO(). + some were introduced very recently and often automatically setup by core library and backends, so for those we are exceptionally not maintaining a legacy redirection symbol. + - commented the old ImageButton() signature obsoleted in 1.89 (~August 2022). As a reminder: + - old ImageButton() before 1.89 used ImTextureId as item id (created issue with e.g. multiple buttons in same scope, transient texture id values, opaque computation of ID) + - new ImageButton() since 1.89 requires an explicit 'const char* str_id' + - old ImageButton() before 1.89 had frame_padding' override argument. + - new ImageButton() since 1.89 always use style.FramePadding, which you can freely override with PushStyleVar()/PopStyleVar(). + - 2024/07/25 (1.91.0) - obsoleted GetContentRegionMax(), GetWindowContentRegionMin() and GetWindowContentRegionMax(). (see #7838 on GitHub for more info) + you should never need those functions. you can do everything with GetCursorScreenPos() and GetContentRegionAvail() in a more simple way. + - instead of: GetWindowContentRegionMax().x - GetCursorPos().x + - you can use: GetContentRegionAvail().x + - instead of: GetWindowContentRegionMax().x + GetWindowPos().x + - you can use: GetCursorScreenPos().x + GetContentRegionAvail().x // when called from left edge of window + - instead of: GetContentRegionMax() + - you can use: GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos() // right edge in local coordinates + - instead of: GetWindowContentRegionMax().x - GetWindowContentRegionMin().x + - you can use: GetContentRegionAvail() // when called from left edge of window + - 2024/07/15 (1.91.0) - renamed ImGuiSelectableFlags_DontClosePopups to ImGuiSelectableFlags_NoAutoClosePopups. (#1379, #1468, #2200, #4936, #5216, #7302, #7573) + (internals: also renamed ImGuiItemFlags_SelectableDontClosePopup into ImGuiItemFlags_AutoClosePopups with inverted behaviors) + - 2024/07/15 (1.91.0) - obsoleted PushButtonRepeat()/PopButtonRepeat() in favor of using new PushItemFlag(ImGuiItemFlags_ButtonRepeat, ...)/PopItemFlag(). + - 2024/07/02 (1.91.0) - commented out obsolete ImGuiModFlags (renamed to ImGuiKeyChord in 1.89). (#4921, #456) + - commented out obsolete ImGuiModFlags_XXX values (renamed to ImGuiMod_XXX in 1.89). (#4921, #456) + - ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl, ImGuiModFlags_Shift -> ImGuiMod_Shift etc. + - 2024/07/02 (1.91.0) - IO, IME: renamed platform IME hook and added explicit context for consistency and future-proofness. + - old: io.SetPlatformImeDataFn(ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - new: io.PlatformSetImeDataFn(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - 2024/06/21 (1.90.9) - BeginChild: added ImGuiChildFlags_NavFlattened as a replacement for the window flag ImGuiWindowFlags_NavFlattened: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_NavFlattened); + - new: BeginChild("Name", size, ImGuiChildFlags_NavFlattened, 0); + - 2024/06/21 (1.90.9) - io: ClearInputKeys() (first exposed in 1.89.8) doesn't clear mouse data, newly added ClearInputMouse() does. + - 2024/06/20 (1.90.9) - renamed ImGuiDragDropFlags_SourceAutoExpirePayload to ImGuiDragDropFlags_PayloadAutoExpire. + - 2024/06/18 (1.90.9) - style: renamed ImGuiCol_TabActive -> ImGuiCol_TabSelected, ImGuiCol_TabUnfocused -> ImGuiCol_TabDimmed, ImGuiCol_TabUnfocusedActive -> ImGuiCol_TabDimmedSelected. + - 2024/06/10 (1.90.9) - removed old nested structure: renaming ImGuiStorage::ImGuiStoragePair type to ImGuiStoragePair (simpler for many languages). + - 2024/06/06 (1.90.8) - reordered ImGuiInputTextFlags values. This should not be breaking unless you are using generated headers that have values not matching the main library. + - 2024/06/06 (1.90.8) - removed 'ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft', was mostly unused and misleading. + - 2024/05/27 (1.90.7) - commented out obsolete symbols marked obsolete in 1.88 (May 2022): + - old: CaptureKeyboardFromApp(bool) + - new: SetNextFrameWantCaptureKeyboard(bool) + - old: CaptureMouseFromApp(bool) + - new: SetNextFrameWantCaptureMouse(bool) + - 2024/05/22 (1.90.7) - inputs (internals): renamed ImGuiKeyOwner_None to ImGuiKeyOwner_NoOwner, to make use more explicit and reduce confusion with the default it is a non-zero value and cannot be the default value (never made public, but disclosing as I expect a few users caught on owner-aware inputs). + - inputs (internals): renamed ImGuiInputFlags_RouteGlobalLow -> ImGuiInputFlags_RouteGlobal, ImGuiInputFlags_RouteGlobal -> ImGuiInputFlags_RouteGlobalOverFocused, ImGuiInputFlags_RouteGlobalHigh -> ImGuiInputFlags_RouteGlobalHighest. + - inputs (internals): Shortcut(), SetShortcutRouting(): swapped last two parameters order in function signatures: + - old: Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id = 0, ImGuiInputFlags flags = 0); + - new: Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0, ImGuiID owner_id = 0); + - inputs (internals): owner-aware versions of IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(): swapped last two parameters order in function signatures. + - old: IsKeyPressed(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); + - old: IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + for various reasons those changes makes sense. They are being made because making some of those API public. + only past users of imgui_internal.h with the extra parameters will be affected. Added asserts for valid flags in various functions to detect _some_ misuses, BUT NOT ALL. + - 2024/05/16 (1.90.7) - inputs: on macOS X, Cmd and Ctrl keys are now automatically swapped by io.AddKeyEvent() as this naturally align with how macOS X uses those keys. + - it shouldn't really affect you unless you had custom shortcut swapping in place for macOS X apps. + - removed ImGuiMod_Shortcut which was previously dynamically remapping to Ctrl or Cmd/Super. It is now unnecessary to specific cross-platform idiomatic shortcuts. (#2343, #4084, #5923, #456) + - 2024/05/14 (1.90.7) - backends: SDL_Renderer2 and SDL_Renderer3 backend now take a SDL_Renderer* in their RenderDrawData() functions. + - 2024/04/18 (1.90.6) - TreeNode: Fixed a layout inconsistency when using an empty/hidden label followed by a SameLine() call. (#7505, #282) + - old: TreeNode("##Hidden"); SameLine(); Text("Hello"); // <-- This was actually incorrect! BUT appeared to look ok with the default style where ItemSpacing.x == FramePadding.x * 2 (it didn't look aligned otherwise). + - new: TreeNode("##Hidden"); SameLine(0, 0); Text("Hello"); // <-- This is correct for all styles values. + with the fix, IF you were successfully using TreeNode("")+SameLine(); you will now have extra spacing between your TreeNode and the following item. + You'll need to change the SameLine() call to SameLine(0,0) to remove this extraneous spacing. This seemed like the more sensible fix that's not making things less consistent. + (Note: when using this idiom you are likely to also use ImGuiTreeNodeFlags_SpanAvailWidth). + - 2024/03/18 (1.90.5) - merged the radius_x/radius_y parameters in ImDrawList::AddEllipse(), AddEllipseFilled() and PathEllipticalArcTo() into a single ImVec2 parameter. Exceptionally, because those functions were added in 1.90, we are not adding inline redirection functions. The transition is easy and should affect few users. (#2743, #7417) + - 2024/03/08 (1.90.5) - inputs: more formally obsoleted GetKeyIndex() when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is set. It has been unnecessary and a no-op since 1.87 (it returns the same value as passed when used with a 1.87+ backend using io.AddKeyEvent() function). (#4921) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - 2024/01/15 (1.90.2) - commented out obsolete ImGuiIO::ImeWindowHandle marked obsolete in 1.87, favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2023/12/19 (1.90.1) - commented out obsolete ImGuiKey_KeyPadEnter redirection to ImGuiKey_KeypadEnter. + - 2023/11/06 (1.90.1) - removed CalcListClipping() marked obsolete in 1.86. Prefer using ImGuiListClipper which can return non-contiguous ranges. + - 2023/11/05 (1.90.1) - imgui_freetype: commented out ImGuiFreeType::BuildFontAtlas() obsoleted in 1.81. prefer using #define IMGUI_ENABLE_FREETYPE or see commented code for manual calls. + - 2023/11/05 (1.90.1) - internals,columns: commented out legacy ImGuiColumnsFlags_XXX symbols redirecting to ImGuiOldColumnsFlags_XXX, obsoleted from imgui_internal.h in 1.80. + - 2023/11/09 (1.90.0) - removed IM_OFFSETOF() macro in favor of using offsetof() available in C++11. Kept redirection define (will obsolete). + - 2023/11/07 (1.90.0) - removed BeginChildFrame()/EndChildFrame() in favor of using BeginChild() with the ImGuiChildFlags_FrameStyle flag. kept inline redirection function (will obsolete). + those functions were merely PushStyle/PopStyle helpers, the removal isn't so much motivated by needing to add the feature in BeginChild(), but by the necessity to avoid BeginChildFrame() signature mismatching BeginChild() signature and features. + - 2023/11/02 (1.90.0) - BeginChild: upgraded 'bool border = true' parameter to 'ImGuiChildFlags flags' type, added ImGuiChildFlags_Border equivalent. As with our prior "bool-to-flags" API updates, the ImGuiChildFlags_Border value is guaranteed to be == true forever to ensure a smoother transition, meaning all existing calls will still work. + - old: BeginChild("Name", size, true) + - new: BeginChild("Name", size, ImGuiChildFlags_Border) + - old: BeginChild("Name", size, false) + - new: BeginChild("Name", size) or BeginChild("Name", 0) or BeginChild("Name", size, ImGuiChildFlags_None) + **AMEND FROM THE FUTURE: from 1.91.1, 'ImGuiChildFlags_Border' is called 'ImGuiChildFlags_Borders'** + - 2023/11/02 (1.90.0) - BeginChild: added child-flag ImGuiChildFlags_AlwaysUseWindowPadding as a replacement for the window-flag ImGuiWindowFlags_AlwaysUseWindowPadding: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_AlwaysUseWindowPadding); + - new: BeginChild("Name", size, ImGuiChildFlags_AlwaysUseWindowPadding, 0); + - 2023/09/27 (1.90.0) - io: removed io.MetricsActiveAllocations introduced in 1.63. Same as 'g.DebugMemAllocCount - g.DebugMemFreeCount' (still displayed in Metrics, unlikely to be accessed by end-user). + - 2023/09/26 (1.90.0) - debug tools: Renamed ShowStackToolWindow() ("Stack Tool") to ShowIDStackToolWindow() ("ID Stack Tool"), as earlier name was misleading. Kept inline redirection function. (#4631) + - 2023/09/15 (1.90.0) - ListBox, Combo: changed signature of "name getter" callback in old one-liner ListBox()/Combo() apis. kept inline redirection function (will obsolete). + - old: bool Combo(const char* label, int* current_item, bool (*getter)(void* user_data, int idx, const char** out_text), ...) + - new: bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - old: bool ListBox(const char* label, int* current_item, bool (*getting)(void* user_data, int idx, const char** out_text), ...); + - new: bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - 2023/09/08 (1.90.0) - commented out obsolete redirecting functions: + - GetWindowContentRegionWidth() -> use GetWindowContentRegionMax().x - GetWindowContentRegionMin().x. Consider that generally 'GetContentRegionAvail().x' is more useful. + - ImDrawCornerFlags_XXX -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + grep commented names in sources. + - commented out runtime support for hardcoded ~0 or 0x01..0x0F rounding flags values for AddRect()/AddRectFilled()/PathRect()/AddImageRounded() -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + - 2023/08/25 (1.89.9) - clipper: Renamed IncludeRangeByIndices() (also called ForceDisplayRangeByIndices() before 1.89.6) to IncludeItemsByIndex(). Kept inline redirection function. Sorry! + - 2023/07/12 (1.89.8) - ImDrawData: CmdLists now owned, changed from ImDrawList** to ImVector. Majority of users shouldn't be affected, but you cannot compare to NULL nor reassign manually anymore. Instead use AddDrawList(). (#6406, #4879, #1878) + - 2023/06/28 (1.89.7) - overlapping items: obsoleted 'SetItemAllowOverlap()' (called after item) in favor of calling 'SetNextItemAllowOverlap()' (called before item). 'SetItemAllowOverlap()' didn't and couldn't work reliably since 1.89 (2022-11-15). + - 2023/06/28 (1.89.7) - overlapping items: renamed 'ImGuiTreeNodeFlags_AllowItemOverlap' to 'ImGuiTreeNodeFlags_AllowOverlap', 'ImGuiSelectableFlags_AllowItemOverlap' to 'ImGuiSelectableFlags_AllowOverlap'. Kept redirecting enums (will obsolete). + - 2023/06/28 (1.89.7) - overlapping items: IsItemHovered() now by default return false when querying an item using AllowOverlap mode which is being overlapped. Use ImGuiHoveredFlags_AllowWhenOverlappedByItem to revert to old behavior. + - 2023/06/28 (1.89.7) - overlapping items: Selectable and TreeNode don't allow overlap when active so overlapping widgets won't appear as hovered. While this fixes a common small visual issue, it also means that calling IsItemHovered() after a non-reactive elements - e.g. Text() - overlapping an active one may fail if you don't use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem). (#6610) + - 2023/06/20 (1.89.7) - moved io.HoverDelayShort/io.HoverDelayNormal to style.HoverDelayShort/style.HoverDelayNormal. As the fields were added in 1.89 and expected to be left unchanged by most users, or only tweaked once during app initialization, we are exceptionally accepting the breakage. + - 2023/05/30 (1.89.6) - backends: renamed "imgui_impl_sdlrenderer.cpp" to "imgui_impl_sdlrenderer2.cpp" and "imgui_impl_sdlrenderer.h" to "imgui_impl_sdlrenderer2.h". This is in prevision for the future release of SDL3. + - 2023/05/22 (1.89.6) - listbox: commented out obsolete/redirecting functions that were marked obsolete more than two years ago: + - ListBoxHeader() -> use BeginListBox() (note how two variants of ListBoxHeader() existed. Check commented versions in imgui.h for reference) + - ListBoxFooter() -> use EndListBox() + - 2023/05/15 (1.89.6) - clipper: commented out obsolete redirection constructor 'ImGuiListClipper(int items_count, float items_height = -1.0f)' that was marked obsolete in 1.79. Use default constructor + clipper.Begin(). + - 2023/05/15 (1.89.6) - clipper: renamed ImGuiListClipper::ForceDisplayRangeByIndices() to ImGuiListClipper::IncludeRangeByIndices(). + - 2023/03/14 (1.89.4) - commented out redirecting enums/functions names that were marked obsolete two years ago: + - ImGuiSliderFlags_ClampOnInput -> use ImGuiSliderFlags_AlwaysClamp + - ImGuiInputTextFlags_AlwaysInsertMode -> use ImGuiInputTextFlags_AlwaysOverwrite + - ImDrawList::AddBezierCurve() -> use ImDrawList::AddBezierCubic() + - ImDrawList::PathBezierCurveTo() -> use ImDrawList::PathBezierCubicCurveTo() + - 2023/03/09 (1.89.4) - renamed PushAllowKeyboardFocus()/PopAllowKeyboardFocus() to PushTabStop()/PopTabStop(). Kept inline redirection functions (will obsolete). + - 2023/03/09 (1.89.4) - tooltips: Added 'bool' return value to BeginTooltip() for API consistency. Please only submit contents and call EndTooltip() if BeginTooltip() returns true. In reality the function will _currently_ always return true, but further changes down the line may change this, best to clarify API sooner. + - 2023/02/15 (1.89.4) - moved the optional "courtesy maths operators" implementation from imgui_internal.h in imgui.h. + Even though we encourage using your own maths types and operators by setting up IM_VEC2_CLASS_EXTRA, + it has been frequently requested by people to use our own. We had an opt-in define which was + previously fulfilled in imgui_internal.h. It is now fulfilled in imgui.h. (#6164) + - OK: #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui.h" / #include "imgui_internal.h" + - Error: #include "imgui.h" / #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui_internal.h" + - 2023/02/07 (1.89.3) - backends: renamed "imgui_impl_sdl.cpp" to "imgui_impl_sdl2.cpp" and "imgui_impl_sdl.h" to "imgui_impl_sdl2.h". (#6146) This is in prevision for the future release of SDL3. + - 2022/10/26 (1.89) - commented out redirecting OpenPopupContextItem() which was briefly the name of OpenPopupOnItemClick() from 1.77 to 1.79. + - 2022/10/12 (1.89) - removed runtime patching of invalid "%f"/"%0.f" format strings for DragInt()/SliderInt(). This was obsoleted in 1.61 (May 2018). See 1.61 changelog for details. + - 2022/09/26 (1.89) - renamed and merged keyboard modifiers key enums and flags into a same set. Kept inline redirection enums (will obsolete). + - ImGuiKey_ModCtrl and ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl + - ImGuiKey_ModShift and ImGuiModFlags_Shift -> ImGuiMod_Shift + - ImGuiKey_ModAlt and ImGuiModFlags_Alt -> ImGuiMod_Alt + - ImGuiKey_ModSuper and ImGuiModFlags_Super -> ImGuiMod_Super + the ImGuiKey_ModXXX were introduced in 1.87 and mostly used by backends. + the ImGuiModFlags_XXX have been exposed in imgui.h but not really used by any public api only by third-party extensions. + exceptionally commenting out the older ImGuiKeyModFlags_XXX names ahead of obsolescence schedule to reduce confusion and because they were not meant to be used anyway. + - 2022/09/20 (1.89) - ImGuiKey is now a typed enum, allowing ImGuiKey_XXX symbols to be named in debuggers. + this will require uses of legacy backend-dependent indices to be casted, e.g. + - with imgui_impl_glfw: IsKeyPressed(GLFW_KEY_A) -> IsKeyPressed((ImGuiKey)GLFW_KEY_A); + - with imgui_impl_win32: IsKeyPressed('A') -> IsKeyPressed((ImGuiKey)'A') + - etc. However if you are upgrading code you might well use the better, backend-agnostic IsKeyPressed(ImGuiKey_A) now! + - 2022/09/12 (1.89) - removed the bizarre legacy default argument for 'TreePush(const void* ptr = NULL)', always pass a pointer value explicitly. NULL/nullptr is ok but require cast, e.g. TreePush((void*)nullptr); + - 2022/09/05 (1.89) - commented out redirecting functions/enums names that were marked obsolete in 1.77 and 1.78 (June 2020): + - DragScalar(), DragScalarN(), DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - SliderScalar(), SliderScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - BeginPopupContextWindow(const char*, ImGuiMouseButton, bool) -> use BeginPopupContextWindow(const char*, ImGuiPopupFlags) + - 2022/09/02 (1.89) - obsoleted using SetCursorPos()/SetCursorScreenPos() to extend parent window/cell boundaries. + this relates to when moving the cursor position beyond current boundaries WITHOUT submitting an item. + - previously this would make the window content size ~200x200: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); + - instead, please submit an item: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); + - alternative: + Begin(...) + Dummy(ImVec2(200,200)) + End(); + - content size is now only extended when submitting an item! + - with '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will now be detected and assert. + - without '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will silently be fixed until we obsolete it. + - 2022/08/03 (1.89) - changed signature of ImageButton() function. Kept redirection function (will obsolete). + - added 'const char* str_id' parameter + removed 'int frame_padding = -1' parameter. + - old signature: bool ImageButton(ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), int frame_padding = -1, ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - used the ImTextureID value to create an ID. This was inconsistent with other functions, led to ID conflicts, and caused problems with engines using transient ImTextureID values. + - had a FramePadding override which was inconsistent with other functions and made the already-long signature even longer. + - new signature: bool ImageButton(const char* str_id, ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - requires an explicit identifier. You may still use e.g. PushID() calls and then pass an empty identifier. + - always uses style.FramePadding for padding, to be consistent with other buttons. You may use PushStyleVar() to alter this. + - 2022/07/08 (1.89) - inputs: removed io.NavInputs[] and ImGuiNavInput enum (following 1.87 changes). + - Official backends from 1.87+ -> no issue. + - Official backends from 1.60 to 1.86 -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need updating! + - Custom backends not writing to io.NavInputs[] -> no issue. + - Custom backends writing to io.NavInputs[] -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need fixing! + - TL;DR: Backends should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values instead of filling io.NavInput[]. + - 2022/06/15 (1.88) - renamed IMGUI_DISABLE_METRICS_WINDOW to IMGUI_DISABLE_DEBUG_TOOLS for correctness. kept support for old define (will obsolete). + - 2022/05/03 (1.88) - backends: osx: removed ImGui_ImplOSX_HandleEvent() from backend API in favor of backend automatically handling event capture. All ImGui_ImplOSX_HandleEvent() calls should be removed as they are now unnecessary. + - 2022/04/05 (1.88) - inputs: renamed ImGuiKeyModFlags to ImGuiModFlags. Kept inline redirection enums (will obsolete). This was never used in public API functions but technically present in imgui.h and ImGuiIO. + - 2022/01/20 (1.87) - inputs: reworded gamepad IO. + - Backend writing to io.NavInputs[] -> backend should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values. + - 2022/01/19 (1.87) - sliders, drags: removed support for legacy arithmetic operators (+,+-,*,/) when inputing text. This doesn't break any api/code but a feature that used to be accessible by end-users (which seemingly no one used). + - 2022/01/17 (1.87) - inputs: reworked mouse IO. + - Backend writing to io.MousePos -> backend should call io.AddMousePosEvent() + - Backend writing to io.MouseDown[] -> backend should call io.AddMouseButtonEvent() + - Backend writing to io.MouseWheel -> backend should call io.AddMouseWheelEvent() + - Backend writing to io.MouseHoveredViewport -> backend should call io.AddMouseViewportEvent() [Docking branch w/ multi-viewports only] + note: for all calls to IO new functions, the Dear ImGui context should be bound/current. + read https://github.com/ocornut/imgui/issues/4921 for details. + - 2022/01/10 (1.87) - inputs: reworked keyboard IO. Removed io.KeyMap[], io.KeysDown[] in favor of calling io.AddKeyEvent(). Removed GetKeyIndex(), now unnecessary. All IsKeyXXX() functions now take ImGuiKey values. All features are still functional until IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Read Changelog and Release Notes for details. + - IsKeyPressed(MY_NATIVE_KEY_XXX) -> use IsKeyPressed(ImGuiKey_XXX) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() (+ call io.SetKeyEventNativeData() if you want legacy user code to stil function with legacy key codes). + - Backend writing to io.KeyCtrl, io.KeyShift.. -> backend should call io.AddKeyEvent() with ImGuiMod_XXX values. *IF YOU PULLED CODE BETWEEN 2021/01/10 and 2021/01/27: We used to have a io.AddKeyModsEvent() function which was now replaced by io.AddKeyEvent() with ImGuiMod_XXX values.* + - one case won't work with backward compatibility: if your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") because those values are now larger than the legacy KeyDown[] array. Will assert. + - inputs: added ImGuiKey_ModCtrl/ImGuiKey_ModShift/ImGuiKey_ModAlt/ImGuiKey_ModSuper values to submit keyboard modifiers using io.AddKeyEvent(), instead of writing directly to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper. + - 2022/01/05 (1.87) - inputs: renamed ImGuiKey_KeyPadEnter to ImGuiKey_KeypadEnter to align with new symbols. Kept redirection enum. + - 2022/01/05 (1.87) - removed io.ImeSetInputScreenPosFn() in favor of more flexible io.SetPlatformImeDataFn(). Removed 'void* io.ImeWindowHandle' in favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2022/01/01 (1.87) - commented out redirecting functions/enums names that were marked obsolete in 1.69, 1.70, 1.71, 1.72 (March-July 2019) + - ImGui::SetNextTreeNodeOpen() -> use ImGui::SetNextItemOpen() + - ImGui::GetContentRegionAvailWidth() -> use ImGui::GetContentRegionAvail().x + - ImGui::TreeAdvanceToLabelPos() -> use ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetTreeNodeToLabelSpacing()); + - ImFontAtlas::CustomRect -> use ImFontAtlasCustomRect + - ImGuiColorEditFlags_RGB/HSV/HEX -> use ImGuiColorEditFlags_DisplayRGB/HSV/Hex + - 2021/12/20 (1.86) - backends: removed obsolete Marmalade backend (imgui_impl_marmalade.cpp) + example. Find last supported version at https://github.com/ocornut/imgui/wiki/Bindings + - 2021/11/04 (1.86) - removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. Please open an issue if you think you really need this function. + - 2021/08/23 (1.85) - removed GetWindowContentRegionWidth() function. keep inline redirection helper. can use 'GetWindowContentRegionMax().x - GetWindowContentRegionMin().x' instead for generally 'GetContentRegionAvail().x' is more useful. + - 2021/07/26 (1.84) - commented out redirecting functions/enums names that were marked obsolete in 1.67 and 1.69 (March 2019): + - ImGui::GetOverlayDrawList() -> use ImGui::GetForegroundDrawList() + - ImFont::GlyphRangesBuilder -> use ImFontGlyphRangesBuilder + - 2021/05/19 (1.83) - backends: obsoleted direct access to ImDrawCmd::TextureId in favor of calling ImDrawCmd::GetTexID(). + - if you are using official backends from the source tree: you have nothing to do. + - if you have copied old backend code or using your own: change access to draw_cmd->TextureId to draw_cmd->GetTexID(). + - 2021/03/12 (1.82) - upgraded ImDrawList::AddRect(), AddRectFilled(), PathRect() to use ImDrawFlags instead of ImDrawCornersFlags. + - ImDrawCornerFlags_TopLeft -> use ImDrawFlags_RoundCornersTopLeft + - ImDrawCornerFlags_BotRight -> use ImDrawFlags_RoundCornersBottomRight + - ImDrawCornerFlags_None -> use ImDrawFlags_RoundCornersNone etc. + flags now sanely defaults to 0 instead of 0x0F, consistent with all other flags in the API. + breaking: the default with rounding > 0.0f is now "round all corners" vs old implicit "round no corners": + - rounding == 0.0f + flags == 0 --> meant no rounding --> unchanged (common use) + - rounding > 0.0f + flags != 0 --> meant rounding --> unchanged (common use) + - rounding == 0.0f + flags != 0 --> meant no rounding --> unchanged (unlikely use) + - rounding > 0.0f + flags == 0 --> meant no rounding --> BREAKING (unlikely use): will now round all corners --> use ImDrawFlags_RoundCornersNone or rounding == 0.0f. + this ONLY matters for hard coded use of 0 + rounding > 0.0f. Use of named ImDrawFlags_RoundCornersNone (new) or ImDrawCornerFlags_None (old) are ok. + the old ImDrawCornersFlags used awkward default values of ~0 or 0xF (4 lower bits set) to signify "round all corners" and we sometimes encouraged using them as shortcuts. + legacy path still support use of hard coded ~0 or any value from 0x1 or 0xF. They will behave the same with legacy paths enabled (will assert otherwise). + - 2021/03/11 (1.82) - removed redirecting functions/enums names that were marked obsolete in 1.66 (September 2018): + - ImGui::SetScrollHere() -> use ImGui::SetScrollHereY() + - 2021/03/11 (1.82) - clarified that ImDrawList::PathArcTo(), ImDrawList::PathArcToFast() won't render with radius < 0.0f. Previously it sorts of accidentally worked but would generally lead to counter-clockwise paths and have an effect on anti-aliasing. + - 2021/03/10 (1.82) - upgraded ImDrawList::AddPolyline() and PathStroke() "bool closed" parameter to "ImDrawFlags flags". The matching ImDrawFlags_Closed value is guaranteed to always stay == 1 in the future. + - 2021/02/22 (1.82) - (*undone in 1.84*) win32+mingw: Re-enabled IME functions by default even under MinGW. In July 2016, issue #738 had me incorrectly disable those default functions for MinGW. MinGW users should: either link with -limm32, either set their imconfig file with '#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS'. + - 2021/02/17 (1.82) - renamed rarely used style.CircleSegmentMaxError (old default = 1.60f) to style.CircleTessellationMaxError (new default = 0.30f) as the meaning of the value changed. + - 2021/02/03 (1.81) - renamed ListBoxHeader(const char* label, ImVec2 size) to BeginListBox(). Kept inline redirection function (will obsolete). + - removed ListBoxHeader(const char* label, int items_count, int height_in_items = -1) in favor of specifying size. Kept inline redirection function (will obsolete). + - renamed ListBoxFooter() to EndListBox(). Kept inline redirection function (will obsolete). + - 2021/01/26 (1.81) - removed ImGuiFreeType::BuildFontAtlas(). Kept inline redirection function. Prefer using '#define IMGUI_ENABLE_FREETYPE', but there's a runtime selection path available too. The shared extra flags parameters (very rarely used) are now stored in ImFontAtlas::FontBuilderFlags. + - renamed ImFontConfig::RasterizerFlags (used by FreeType) to ImFontConfig::FontBuilderFlags. + - renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.63 (August 2018): + - ImGui::IsItemDeactivatedAfterChange() -> use ImGui::IsItemDeactivatedAfterEdit(). + - ImGuiCol_ModalWindowDarkening -> use ImGuiCol_ModalWindowDimBg + - ImGuiInputTextCallback -> use ImGuiTextEditCallback + - ImGuiInputTextCallbackData -> use ImGuiTextEditCallbackData + - 2020/12/21 (1.80) - renamed ImDrawList::AddBezierCurve() to AddBezierCubic(), and PathBezierCurveTo() to PathBezierCubicCurveTo(). Kept inline redirection function (will obsolete). + - 2020/12/04 (1.80) - added imgui_tables.cpp file! Manually constructed project files will need the new file added! + - 2020/11/18 (1.80) - renamed undocumented/internals ImGuiColumnsFlags_* to ImGuiOldColumnFlags_* in prevision of incoming Tables API. + - 2020/11/03 (1.80) - renamed io.ConfigWindowsMemoryCompactTimer to io.ConfigMemoryCompactTimer as the feature will apply to other data structures + - 2020/10/14 (1.80) - backends: moved all backends files (imgui_impl_XXXX.cpp, imgui_impl_XXXX.h) from examples/ to backends/. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.60 (April 2018): + - io.RenderDrawListsFn pointer -> use ImGui::GetDrawData() value and call the render function of your backend + - ImGui::IsAnyWindowFocused() -> use ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) + - ImGui::IsAnyWindowHovered() -> use ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + - ImGuiStyleVar_Count_ -> use ImGuiStyleVar_COUNT + - ImGuiMouseCursor_Count_ -> use ImGuiMouseCursor_COUNT + - removed redirecting functions names that were marked obsolete in 1.61 (May 2018): + - InputFloat (... int decimal_precision ...) -> use InputFloat (... const char* format ...) with format = "%.Xf" where X is your value for decimal_precision. + - same for InputFloat2()/InputFloat3()/InputFloat4() variants taking a `int decimal_precision` parameter. + - 2020/10/05 (1.79) - removed ImGuiListClipper: Renamed constructor parameters which created an ambiguous alternative to using the ImGuiListClipper::Begin() function, with misleading edge cases (note: imgui_memory_editor <0.40 from imgui_club/ used this old clipper API. Update your copy if needed). + - 2020/09/25 (1.79) - renamed ImGuiSliderFlags_ClampOnInput to ImGuiSliderFlags_AlwaysClamp. Kept redirection enum (will obsolete sooner because previous name was added recently). + - 2020/09/25 (1.79) - renamed style.TabMinWidthForUnselectedCloseButton to style.TabMinWidthForCloseButton. + - 2020/09/21 (1.79) - renamed OpenPopupContextItem() back to OpenPopupOnItemClick(), reverting the change from 1.77. For varieties of reason this is more self-explanatory. + - 2020/09/21 (1.79) - removed return value from OpenPopupOnItemClick() - returned true on mouse release on an item - because it is inconsistent with other popup APIs and makes others misleading. It's also and unnecessary: you can use IsWindowAppearing() after BeginPopup() for a similar result. + - 2020/09/17 (1.79) - removed ImFont::DisplayOffset in favor of ImFontConfig::GlyphOffset. DisplayOffset was applied after scaling and not very meaningful/useful outside of being needed by the default ProggyClean font. If you scaled this value after calling AddFontDefault(), this is now done automatically. It was also getting in the way of better font scaling, so let's get rid of it now! + - 2020/08/17 (1.78) - obsoleted use of the trailing 'float power=1.0f' parameter for DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(), DragFloatRange2(), DragScalar(), DragScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(), SliderScalar(), SliderScalarN(), VSliderFloat() and VSliderScalar(). + replaced the 'float power=1.0f' argument with integer-based flags defaulting to 0 (as with all our flags). + worked out a backward-compatibility scheme so hopefully most C++ codebase should not be affected. in short, when calling those functions: + - if you omitted the 'power' parameter (likely!), you are not affected. + - if you set the 'power' parameter to 1.0f (same as previous default value): 1/ your compiler may warn on float>int conversion, 2/ everything else will work. 3/ you can replace the 1.0f value with 0 to fix the warning, and be technically correct. + - if you set the 'power' parameter to >1.0f (to enable non-linear editing): 1/ your compiler may warn on float>int conversion, 2/ code will assert at runtime, 3/ in case asserts are disabled, the code will not crash and enable the _Logarithmic flag. 4/ you can replace the >1.0f value with ImGuiSliderFlags_Logarithmic to fix the warning/assert and get a _similar_ effect as previous uses of power >1.0f. + see https://github.com/ocornut/imgui/issues/3361 for all details. + kept inline redirection functions (will obsolete) apart for: DragFloatRange2(), VSliderFloat(), VSliderScalar(). For those three the 'float power=1.0f' version was removed directly as they were most unlikely ever used. + for shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + - obsoleted use of v_min > v_max in DragInt, DragFloat, DragScalar to lock edits (introduced in 1.73, was not demoed nor documented very), will be replaced by a more generic ReadOnly feature. You may use the ImGuiSliderFlags_ReadOnly internal flag in the meantime. + - 2020/06/23 (1.77) - removed BeginPopupContextWindow(const char*, int mouse_button, bool also_over_items) in favor of BeginPopupContextWindow(const char*, ImGuiPopupFlags flags) with ImGuiPopupFlags_NoOverItems. + - 2020/06/15 (1.77) - renamed OpenPopupOnItemClick() to OpenPopupContextItem(). Kept inline redirection function (will obsolete). [NOTE: THIS WAS REVERTED IN 1.79] + - 2020/06/15 (1.77) - removed CalcItemRectClosestPoint() entry point which was made obsolete and asserting in December 2017. + - 2020/04/23 (1.77) - removed unnecessary ID (first arg) of ImFontAtlas::AddCustomRectRegular(). + - 2020/01/22 (1.75) - ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius any more. + - 2019/12/17 (1.75) - [undid this change in 1.76] made Columns() limited to 64 columns by asserting above that limit. While the current code technically supports it, future code may not so we're putting the restriction ahead. + - 2019/12/13 (1.75) - [imgui_internal.h] changed ImRect() default constructor initializes all fields to 0.0f instead of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by adding multiple points into it, you may need to fix your initial value. + - 2019/12/08 (1.75) - removed redirecting functions/enums that were marked obsolete in 1.53 (December 2017): + - ShowTestWindow() -> use ShowDemoWindow() + - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) + - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() + - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg + - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding + - ImGuiTreeNodeFlags_AllowOverlapMode -> use ImGuiTreeNodeFlags_AllowItemOverlap + - IMGUI_DISABLE_TEST_WINDOWS -> use IMGUI_DISABLE_DEMO_WINDOWS + - 2019/12/08 (1.75) - obsoleted calling ImDrawList::PrimReserve() with a negative count (which was vaguely documented and rarely if ever used). Instead, we added an explicit PrimUnreserve() API. + - 2019/12/06 (1.75) - removed implicit default parameter to IsMouseDragging(int button = 0) to be consistent with other mouse functions (none of the other functions have it). + - 2019/11/21 (1.74) - ImFontAtlas::AddCustomRectRegular() now requires an ID larger than 0x110000 (instead of 0x10000) to conform with supporting Unicode planes 1-16 in a future update. ID below 0x110000 will now assert. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS to IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS for consistency. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_MATH_FUNCTIONS to IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS for consistency. + - 2019/10/22 (1.74) - removed redirecting functions/enums that were marked obsolete in 1.52 (October 2017): + - Begin() [old 5 args version] -> use Begin() [3 args], use SetNextWindowSize() SetNextWindowBgAlpha() if needed + - IsRootWindowOrAnyChildHovered() -> use IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) + - AlignFirstTextHeightToWidgets() -> use AlignTextToFramePadding() + - SetNextWindowPosCenter() -> use SetNextWindowPos() with a pivot of (0.5f, 0.5f) + - ImFont::Glyph -> use ImFontGlyph + - 2019/10/14 (1.74) - inputs: Fixed a miscalculation in the keyboard/mouse "typematic" repeat delay/rate calculation, used by keys and e.g. repeating mouse buttons as well as the GetKeyPressedAmount() function. + if you were using a non-default value for io.KeyRepeatRate (previous default was 0.250), you can add +io.KeyRepeatDelay to it to compensate for the fix. + The function was triggering on: 0.0 and (delay+rate*N) where (N>=1). Fixed formula responds to (N>=0). Effectively it made io.KeyRepeatRate behave like it was set to (io.KeyRepeatRate + io.KeyRepeatDelay). + If you never altered io.KeyRepeatRate nor used GetKeyPressedAmount() this won't affect you. + - 2019/07/15 (1.72) - removed TreeAdvanceToLabelPos() which is rarely used and only does SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()). Kept redirection function (will obsolete). + - 2019/07/12 (1.72) - renamed ImFontAtlas::CustomRect to ImFontAtlasCustomRect. Kept redirection typedef (will obsolete). + - 2019/06/14 (1.72) - removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): ImGuiCol_Column*, ImGuiSetCond_*, IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow(), IMGUI_ONCE_UPON_A_FRAME. Grep this log for details and new names, or see how they were implemented until 1.71. + - 2019/06/07 (1.71) - rendering of child window outer decorations (bg color, border, scrollbars) is now performed as part of the parent window. If you have + overlapping child windows in a same parent, and relied on their relative z-order to be mapped to their submission order, this will affect your rendering. + This optimization is disabled if the parent window has no visual output, because it appears to be the most common situation leading to the creation of overlapping child windows. + Please reach out if you are affected. + - 2019/05/13 (1.71) - renamed SetNextTreeNodeOpen() to SetNextItemOpen(). Kept inline redirection function (will obsolete). + - 2019/05/11 (1.71) - changed io.AddInputCharacter(unsigned short c) signature to io.AddInputCharacter(unsigned int c). + - 2019/04/29 (1.70) - improved ImDrawList thick strokes (>1.0f) preserving correct thickness up to 90 degrees angles (e.g. rectangles). If you have custom rendering using thick lines, they will appear thicker now. + - 2019/04/29 (1.70) - removed GetContentRegionAvailWidth(), use GetContentRegionAvail().x instead. Kept inline redirection function (will obsolete). + - 2019/03/04 (1.69) - renamed GetOverlayDrawList() to GetForegroundDrawList(). Kept redirection function (will obsolete). + - 2019/02/26 (1.69) - renamed ImGuiColorEditFlags_RGB/ImGuiColorEditFlags_HSV/ImGuiColorEditFlags_HEX to ImGuiColorEditFlags_DisplayRGB/ImGuiColorEditFlags_DisplayHSV/ImGuiColorEditFlags_DisplayHex. Kept redirection enums (will obsolete). + - 2019/02/14 (1.68) - made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). If for some reason your time step calculation gives you a zero value, replace it with an arbitrarily small value! + - 2019/02/01 (1.68) - removed io.DisplayVisibleMin/DisplayVisibleMax (which were marked obsolete and removed from viewport/docking branch already). + - 2019/01/06 (1.67) - renamed io.InputCharacters[], marked internal as was always intended. Please don't access directly, and use AddInputCharacter() instead! + - 2019/01/06 (1.67) - renamed ImFontAtlas::GlyphRangesBuilder to ImFontGlyphRangesBuilder. Kept redirection typedef (will obsolete). + - 2018/12/20 (1.67) - made it illegal to call Begin("") with an empty string. This somehow half-worked before but had various undesirable side-effects. + - 2018/12/10 (1.67) - renamed io.ConfigResizeWindowsFromEdges to io.ConfigWindowsResizeFromEdges as we are doing a large pass on configuration flags. + - 2018/10/12 (1.66) - renamed misc/stl/imgui_stl.* to misc/cpp/imgui_stdlib.* in prevision for other C++ helper files. + - 2018/09/28 (1.66) - renamed SetScrollHere() to SetScrollHereY(). Kept redirection function (will obsolete). + - 2018/09/06 (1.65) - renamed stb_truetype.h to imstb_truetype.h, stb_textedit.h to imstb_textedit.h, and stb_rect_pack.h to imstb_rectpack.h. + If you were conveniently using the imgui copy of those STB headers in your project you will have to update your include paths. + - 2018/09/05 (1.65) - renamed io.OptCursorBlink/io.ConfigCursorBlink to io.ConfigInputTextCursorBlink. (#1427) + - 2018/08/31 (1.64) - added imgui_widgets.cpp file, extracted and moved widgets code out of imgui.cpp into imgui_widgets.cpp. Re-ordered some of the code remaining in imgui.cpp. + NONE OF THE FUNCTIONS HAVE CHANGED. THE CODE IS SEMANTICALLY 100% IDENTICAL, BUT _EVERY_ FUNCTION HAS BEEN MOVED. + Because of this, any local modifications to imgui.cpp will likely conflict when you update. Read docs/CHANGELOG.txt for suggestions. + - 2018/08/22 (1.63) - renamed IsItemDeactivatedAfterChange() to IsItemDeactivatedAfterEdit() for consistency with new IsItemEdited() API. Kept redirection function (will obsolete soonish as IsItemDeactivatedAfterChange() is very recent). + - 2018/08/21 (1.63) - renamed ImGuiTextEditCallback to ImGuiInputTextCallback, ImGuiTextEditCallbackData to ImGuiInputTextCallbackData for consistency. Kept redirection types (will obsolete). + - 2018/08/21 (1.63) - removed ImGuiInputTextCallbackData::ReadOnly since it is a duplication of (ImGuiInputTextCallbackData::Flags & ImGuiInputTextFlags_ReadOnly). + - 2018/08/01 (1.63) - removed per-window ImGuiWindowFlags_ResizeFromAnySide beta flag in favor of a global io.ConfigResizeWindowsFromEdges [update 1.67 renamed to ConfigWindowsResizeFromEdges] to enable the feature. + - 2018/08/01 (1.63) - renamed io.OptCursorBlink to io.ConfigCursorBlink [-> io.ConfigInputTextCursorBlink in 1.65], io.OptMacOSXBehaviors to ConfigMacOSXBehaviors for consistency. + - 2018/07/22 (1.63) - changed ImGui::GetTime() return value from float to double to avoid accumulating floating point imprecisions over time. + - 2018/07/08 (1.63) - style: renamed ImGuiCol_ModalWindowDarkening to ImGuiCol_ModalWindowDimBg for consistency with other features. Kept redirection enum (will obsolete). + - 2018/06/08 (1.62) - examples: the imgui_impl_XXX files have been split to separate platform (Win32, GLFW, SDL2, etc.) from renderer (DX11, OpenGL, Vulkan, etc.). + old backends will still work as is, however prefer using the separated backends as they will be updated to support multi-viewports. + when adopting new backends follow the main.cpp code of your preferred examples/ folder to know which functions to call. + in particular, note that old backends called ImGui::NewFrame() at the end of their ImGui_ImplXXXX_NewFrame() function. + - 2018/06/06 (1.62) - renamed GetGlyphRangesChinese() to GetGlyphRangesChineseFull() to distinguish other variants and discourage using the full set. + - 2018/06/06 (1.62) - TreeNodeEx()/TreeNodeBehavior(): the ImGuiTreeNodeFlags_CollapsingHeader helper now include the ImGuiTreeNodeFlags_NoTreePushOnOpen flag. See Changelog for details. + - 2018/05/03 (1.61) - DragInt(): the default compile-time format string has been changed from "%.0f" to "%d", as we are not using integers internally any more. + If you used DragInt() with custom format strings, make sure you change them to use %d or an integer-compatible format. + To honor backward-compatibility, the DragInt() code will currently parse and modify format strings to replace %*f with %d, giving time to users to upgrade their code. + If you have IMGUI_DISABLE_OBSOLETE_FUNCTIONS enabled, the code will instead assert! You may run a reg-exp search on your codebase for e.g. "DragInt.*%f" to help you find them. + - 2018/04/28 (1.61) - obsoleted InputFloat() functions taking an optional "int decimal_precision" in favor of an equivalent and more flexible "const char* format", + consistent with other functions. Kept redirection functions (will obsolete). + - 2018/04/09 (1.61) - IM_DELETE() helper function added in 1.60 doesn't clear the input _pointer_ reference, more consistent with expectation and allows passing r-value. + - 2018/03/20 (1.60) - renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, _not_ used by core and only honored by some backend ahead of merging the Nav branch). + - 2018/03/12 (1.60) - removed ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered as the closing cross uses regular button colors now. + - 2018/03/08 (1.60) - changed ImFont::DisplayOffset.y to default to 0 instead of +1. Fixed rounding of Ascent/Descent to match TrueType renderer. If you were adding or subtracting to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. + - 2018/03/03 (1.60) - renamed ImGuiStyleVar_Count_ to ImGuiStyleVar_COUNT and ImGuiMouseCursor_Count_ to ImGuiMouseCursor_COUNT for consistency with other public enums. - 2018/02/18 (1.60) - BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. - 2018/02/16 (1.60) - obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). Use ImGui::GetDrawData() to retrieve the ImDrawData* to display. - 2018/02/07 (1.60) - reorganized context handling to be more explicit, - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. - removed Shutdown() function, as DestroyContext() serve this purpose. - - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwhise CreateContext() will create its own font atlas instance. + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwise CreateContext() will create its own font atlas instance. - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. - 2018/01/31 (1.60) - moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. @@ -283,45 +815,44 @@ - 2017/10/20 (1.52) - changed IsWindowHovered() default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. - 2017/10/20 (1.52) - marked IsItemHoveredRect()/IsMouseHoveringWindow() as obsolete, in favor of using the newly introduced flags for IsItemHovered() and IsWindowHovered(). See https://github.com/ocornut/imgui/issues/1382 for details. removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. + IsItemHoveredRect() --> IsItemHovered(ImGuiHoveredFlags_RectOnly) + IsMouseHoveringAnyWindow() --> IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + IsMouseHoveringWindow() --> IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) [weird, old behavior] - 2017/10/17 (1.52) - marked the old 5-parameters version of Begin() as obsolete (still available). Use SetNextWindowSize()+Begin() instead! - 2017/10/11 (1.52) - renamed AlignFirstTextHeightToWidgets() to AlignTextToFramePadding(). Kept inline redirection function (will obsolete). + - 2017/09/26 (1.52) - renamed ImFont::Glyph to ImFontGlyph. Kept redirection typedef (will obsolete). - 2017/09/25 (1.52) - removed SetNextWindowPosCenter() because SetNextWindowPos() now has the optional pivot information to do the same and more. Kept redirection function (will obsolete). - - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your binding if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". + - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your backend if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". - 2017/08/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency. - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix. - - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame. + - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame type. - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely. - - 2017/08/13 (1.51) - renamed ImGuiCol_Columns*** to ImGuiCol_Separator***. Kept redirection enums (will obsolete). - - 2017/08/11 (1.51) - renamed ImGuiSetCond_*** types and flags to ImGuiCond_***. Kept redirection enums (will obsolete). + - 2017/08/13 (1.51) - renamed ImGuiCol_Column to ImGuiCol_Separator, ImGuiCol_ColumnHovered to ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive to ImGuiCol_SeparatorActive. Kept redirection enums (will obsolete). + - 2017/08/11 (1.51) - renamed ImGuiSetCond_Always to ImGuiCond_Always, ImGuiSetCond_Once to ImGuiCond_Once, ImGuiSetCond_FirstUseEver to ImGuiCond_FirstUseEver, ImGuiSetCond_Appearing to ImGuiCond_Appearing. Kept redirection enums (will obsolete). - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. - - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0))' + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0))' - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. - - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetId() and use it instead of passing string to BeginChild(). + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetID() and use it instead of passing string to BeginChild(). - 2016/10/15 (1.50) - avoid 'void* user_data' parameter to io.SetClipboardTextFn/io.GetClipboardTextFn pointers. We pass io.ClipboardUserData to it. - 2016/09/25 (1.50) - style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. - - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully breakage should be minimal. + - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully, breakage should be minimal. - 2016/05/12 (1.49) - title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore. - If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you. - However if your TitleBg/TitleBgActive alpha was <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. - This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color. - ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) - { - float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; - return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); - } + If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you, otherwise if <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color: + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) { float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); } If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). - - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDraw::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. - - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref github issue #337). + - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDrawList::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref GitHub issue #337). - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. @@ -338,20 +869,13 @@ you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. this necessary change will break your rendering function! the fix should be very easy. sorry for that :( - - if you are using a vanilla copy of one of the imgui_impl_XXXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - if you are using a vanilla copy of one of the imgui_impl_XXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. - the signature of the io.RenderDrawListsFn handler has changed! - ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) - became: - ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). - argument 'cmd_lists' -> 'draw_data->CmdLists' - argument 'cmd_lists_count' -> 'draw_data->CmdListsCount' - ImDrawList 'commands' -> 'CmdBuffer' - ImDrawList 'vtx_buffer' -> 'VtxBuffer' - ImDrawList n/a -> 'IdxBuffer' (new) - ImDrawCmd 'vtx_count' -> 'ElemCount' - ImDrawCmd 'clip_rect' -> 'ClipRect' - ImDrawCmd 'user_callback' -> 'UserCallback' - ImDrawCmd 'texture_id' -> 'TextureId' + old: ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + new: ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). + parameters: 'cmd_lists' becomes 'draw_data->CmdLists', 'cmd_lists_count' becomes 'draw_data->CmdListsCount' + ImDrawList: 'commands' becomes 'CmdBuffer', 'vtx_buffer' becomes 'VtxBuffer', 'IdxBuffer' is new. + ImDrawCmd: 'vtx_count' becomes 'ElemCount', 'clip_rect' becomes 'ClipRect', 'user_callback' becomes 'UserCallback', 'texture_id' becomes 'TextureId'. - each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles using indices from the index buffer. - if you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! - refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. please upgrade! @@ -359,7 +883,7 @@ - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence - - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely be used. Sorry! + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely used. Sorry! - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. @@ -381,23 +905,13 @@ - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. - (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. - this sequence: - const void* png_data; - unsigned int png_size; - ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); - // - became: - unsigned char* pixels; - int width, height; - io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); - // - io.Fonts->TexID = (your_texture_identifier); - you now have much more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. - it is now recommended that you sample the font texture with bilinear interpolation. - (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to set io.Fonts->TexID. - (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) - (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2015/01/11 (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + - old: const void* png_data; unsigned int png_size; ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); [..Upload texture to GPU..]; + - new: unsigned char* pixels; int width, height; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); [..Upload texture to GPU..]; io.Fonts->SetTexID(YourTexIdentifier); + you now have more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. It is now recommended that you sample the font texture with bilinear interpolation. + - 2015/01/11 (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to call io.Fonts->SetTexID() + - 2015/01/11 (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + - 2015/01/11 (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility @@ -411,13356 +925,15459 @@ - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes - ISSUES & TODO-LIST - ================== - See TODO.txt - - - FREQUENTLY ASKED QUESTIONS (FAQ), TIPS - ====================================== - - Q: How can I help? - A: - If you are experienced with Dear ImGui and C++, look at the github issues, or TODO.txt and see how you want/can help! - - Convince your company to fund development time! Individual users: you can also become a Patron (patreon.com/imgui) or donate on PayPal! See README. - - Disclose your usage of dear imgui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. - You may post screenshot or links in the gallery threads (github.com/ocornut/imgui/issues/1269). Visuals are ideal as they inspire other programmers. - But even without visuals, disclosing your use of dear imgui help the library grow credibility, and help other teams and programmers with taking decisions. - - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on github or privately). - - Q: How can I display an image? What is ImTextureID, how does it works? - A: ImTextureID is a void* used to pass renderer-agnostic texture references around until it hits your render function. - Dear ImGui knows nothing about what those bits represent, it just passes them around. It is up to you to decide what you want the void* to carry! - It could be an identifier to your OpenGL texture (cast GLuint to void*), a pointer to your custom engine material (cast MyMaterial* to void*), etc. - At the end of the chain, your renderer takes this void* to cast it back into whatever it needs to select a current texture to render. - Refer to examples applications, where each renderer (in a imgui_impl_xxxx.cpp file) is treating ImTextureID as a different thing. - (c++ tip: OpenGL uses integers to identify textures. You can safely store an integer into a void*, just cast it to void*, don't take it's address!) - To display a custom image/texture within an ImGui window, you may use ImGui::Image(), ImGui::ImageButton(), ImDrawList::AddImage() functions. - Dear ImGui will generate the geometry and draw calls using the ImTextureID that you passed and which your renderer can use. - You may call ImGui::ShowMetricsWindow() to explore active draw lists and visualize/understand how the draw data is generated. - It is your responsibility to get textures uploaded to your GPU. - - Q: Can I have multiple widgets with the same label? Can I have widget without a label? - A: Yes. A primer on labels and the ID stack... - - - Elements that are typically not clickable, such as Text() items don't need an ID. - - - Interactive widgets require state to be carried over multiple frames (most typically Dear ImGui often needs to remember what is - the "active" widget). to do so they need a unique ID. unique ID are typically derived from a string label, an integer index or a pointer. - - Button("OK"); // Label = "OK", ID = hash of "OK" - Button("Cancel"); // Label = "Cancel", ID = hash of "Cancel" - - - ID are uniquely scoped within windows, tree nodes, etc. so no conflict can happen if you have two buttons called "OK" - in two different windows or in two different locations of a tree. - - - If you have a same ID twice in the same location, you'll have a conflict: - - Button("OK"); - Button("OK"); // ID collision! Both buttons will be treated as the same. - - Fear not! this is easy to solve and there are many ways to solve it! - - - When passing a label you can optionally specify extra unique ID information within string itself. - Use "##" to pass a complement to the ID that won't be visible to the end-user. - This helps solving the simple collision cases when you know which items are going to be created. - - Button("Play"); // Label = "Play", ID = hash of "Play" - Button("Play##foo1"); // Label = "Play", ID = hash of "Play##foo1" (different from above) - Button("Play##foo2"); // Label = "Play", ID = hash of "Play##foo2" (different from above) - - - If you want to completely hide the label, but still need an ID: - - Checkbox("##On", &b); // Label = "", ID = hash of "##On" (no label!) - - - Occasionally/rarely you might want change a label while preserving a constant ID. This allows you to animate labels. - For example you may want to include varying information in a window title bar, but windows are uniquely identified by their ID.. - Use "###" to pass a label that isn't part of ID: - - Button("Hello###ID"; // Label = "Hello", ID = hash of "ID" - Button("World###ID"; // Label = "World", ID = hash of "ID" (same as above) - - sprintf(buf, "My game (%f FPS)###MyGame", fps); - Begin(buf); // Variable label, ID = hash of "MyGame" - - - Use PushID() / PopID() to create scopes and avoid ID conflicts within the same Window. - This is the most convenient way of distinguishing ID if you are iterating and creating many UI elements. - You can push a pointer, a string or an integer value. Remember that ID are formed from the concatenation of _everything_ in the ID stack! - - for (int i = 0; i < 100; i++) - { - PushID(i); - Button("Click"); // Label = "Click", ID = hash of integer + "label" (unique) - PopID(); - } - - for (int i = 0; i < 100; i++) - { - MyObject* obj = Objects[i]; - PushID(obj); - Button("Click"); // Label = "Click", ID = hash of pointer + "label" (unique) - PopID(); - } - - for (int i = 0; i < 100; i++) - { - MyObject* obj = Objects[i]; - PushID(obj->Name); - Button("Click"); // Label = "Click", ID = hash of string + "label" (unique) - PopID(); - } - - - More example showing that you can stack multiple prefixes into the ID stack: - - Button("Click"); // Label = "Click", ID = hash of "Click" - PushID("node"); - Button("Click"); // Label = "Click", ID = hash of "node" + "Click" - PushID(my_ptr); - Button("Click"); // Label = "Click", ID = hash of "node" + ptr + "Click" - PopID(); - PopID(); + FREQUENTLY ASKED QUESTIONS (FAQ) + ================================ + + Read all answers online: + https://www.dearimgui.com/faq or https://github.com/ocornut/imgui/blob/master/docs/FAQ.md (same url) + Read all answers locally (with a text editor or ideally a Markdown viewer): + docs/FAQ.md + Some answers are copied down here to facilitate searching in code. + + Q&A: Basics + =========== + + Q: Where is the documentation? + A: This library is poorly documented at the moment and expects the user to be acquainted with C/C++. + - Run the examples/ applications and explore them. + - Read Getting Started (https://github.com/ocornut/imgui/wiki/Getting-Started) guide. + - See demo code in imgui_demo.cpp and particularly the ImGui::ShowDemoWindow() function. + - The demo covers most features of Dear ImGui, so you can read the code and see its output. + - See documentation and comments at the top of imgui.cpp + effectively imgui.h. + - 20+ standalone example applications using e.g. OpenGL/DirectX are provided in the + examples/ folder to explain how to integrate Dear ImGui with your own engine/application. + - The Wiki (https://github.com/ocornut/imgui/wiki) has many resources and links. + - The Glossary (https://github.com/ocornut/imgui/wiki/Glossary) page also may be useful. + - Your programming IDE is your friend, find the type or function declaration to find comments + associated with it. + + Q: What is this library called? + Q: Which version should I get? + >> This library is called "Dear ImGui", please don't call it "ImGui" :) + >> See https://www.dearimgui.com/faq for details. + + Q&A: Integration + ================ - - Tree nodes implicitly creates a scope for you by calling PushID(). + Q: How to get started? + A: Read https://github.com/ocornut/imgui/wiki/Getting-Started. Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. + + Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? + A: You should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + >> See https://www.dearimgui.com/faq for a fully detailed answer. You really want to read this. + + Q. How can I enable keyboard or gamepad controls? + Q: How can I use this on a machine without mouse, keyboard or screen? (input share, remote display) + Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... + Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... + >> See https://www.dearimgui.com/faq + + Q&A: Usage + ---------- + + Q: About the ID Stack system.. + - Why is my widget not reacting when I click on it? + - How can I have widgets with an empty label? + - How can I have multiple widgets with the same label? + - How can I have multiple windows with the same label? + Q: How can I display an image? What is ImTextureID, how does it work? + Q: How can I use my own math types instead of ImVec2? + Q: How can I interact with standard C++ types (such as std::string and std::vector)? + Q: How can I display custom shapes? (using low-level ImDrawList API) + >> See https://www.dearimgui.com/faq + + Q&A: Fonts, Text + ================ - Button("Click"); // Label = "Click", ID = hash of "Click" - if (TreeNode("node")) - { - Button("Click"); // Label = "Click", ID = hash of "node" + "Click" - TreePop(); - } + Q: How should I handle DPI in my application? + Q: How can I load a different font than the default? + Q: How can I easily use icons in my application? + Q: How can I load multiple fonts? + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + >> See https://www.dearimgui.com/faq and https://github.com/ocornut/imgui/blob/master/docs/FONTS.md - - When working with trees, ID are used to preserve the open/close state of each tree node. - Depending on your use cases you may want to use strings, indices or pointers as ID. - e.g. when displaying a single object that may change over time (dynamic 1-1 relationship), using a static string as ID will preserve your - node open/closed state when the targeted object change. - e.g. when displaying a list of objects, using indices or pointers as ID will preserve the node open/closed state differently. - experiment and see what makes more sense! - - Q: How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? - A: You can read the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'ioWantTextInput' flags from the ImGuiIO structure. - - When 'io.WantCaptureMouse' or 'io.WantCaptureKeyboard' flags are set you may want to discard/hide the inputs from the rest of your application. - - When 'io.WantTextInput' is set to may want to notify your OS to popup an on-screen keyboard, if available (e.g. on a mobile phone, or console OS). - Preferably read the flags after calling ImGui::NewFrame() to avoid them lagging by one frame. But reading those flags before calling NewFrame() is - also generally ok, as the bool toggles fairly rarely and you don't generally expect to interact with either Dear ImGui or your application during - the same frame when that transition occurs. Dear ImGui is tracking dragging and widget activity that may occur outside the boundary of a window, - so 'io.WantCaptureMouse' is more accurate and correct than checking if a window is hovered. - (Advanced note: text input releases focus on Return 'KeyDown', so the following Return 'KeyUp' event that your application receive will typically - have 'io.WantCaptureKeyboard=false'. Depending on your application logic it may or not be inconvenient. You might want to track which key-downs - were for Dear ImGui, e.g. with an array of bool, and filter out the corresponding key-ups.) - - Q: How can I load a different font than the default? (default is an embedded version of ProggyClean.ttf, rendered at size 13) - A: Use the font atlas to load the TTF/OTF file you want: - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); - io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() - - New programmers: remember that in C/C++ and most programming languages if you want to use a backslash \ in a string literal you need to write a double backslash "\\": - io.Fonts->AddFontFromFileTTF("MyDataFolder\MyFontFile.ttf", size_in_pixels); // WRONG - io.Fonts->AddFontFromFileTTF("MyDataFolder\\MyFontFile.ttf", size_in_pixels); // CORRECT - io.Fonts->AddFontFromFileTTF("MyDataFolder/MyFontFile.ttf", size_in_pixels); // ALSO CORRECT + Q&A: Concerns + ============= - Q: How can I easily use icons in my application? - A: The most convenient and practical way is to merge an icon font such as FontAwesome inside you main font. Then you can refer to icons within your - strings. Read 'How can I load multiple fonts?' and the file 'misc/fonts/README.txt' for instructions and useful header files. + Q: Who uses Dear ImGui? + Q: Can you create elaborate/serious tools with Dear ImGui? + Q: Can you reskin the look of Dear ImGui? + Q: Why using C++ (as opposed to C)? + >> See https://www.dearimgui.com/faq - Q: How can I load multiple fonts? - A: Use the font atlas to pack them into a single texture: - (Read misc/fonts/README.txt and the code in ImFontAtlas for more details.) - - ImGuiIO& io = ImGui::GetIO(); - ImFont* font0 = io.Fonts->AddFontDefault(); - ImFont* font1 = io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); - ImFont* font2 = io.Fonts->AddFontFromFileTTF("myfontfile2.ttf", size_in_pixels); - io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() - // the first loaded font gets used by default - // use ImGui::PushFont()/ImGui::PopFont() to change the font at runtime - - // Options - ImFontConfig config; - config.OversampleH = 3; - config.OversampleV = 1; - config.GlyphOffset.y -= 2.0f; // Move everything by 2 pixels up - config.GlyphExtraSpacing.x = 1.0f; // Increase spacing between characters - io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, &config); - - // Combine multiple fonts into one (e.g. for icon fonts) - ImWchar ranges[] = { 0xf000, 0xf3ff, 0 }; - ImFontConfig config; - config.MergeMode = true; - io.Fonts->AddFontDefault(); - io.Fonts->LoadFromFileTTF("fontawesome-webfont.ttf", 16.0f, &config, ranges); // Merge icon font - io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs + Q&A: Community + ============== - Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? - A: When loading a font, pass custom Unicode ranges to specify the glyphs to load. - - // Add default Japanese ranges - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); - - // Or create your own custom ranges (e.g. for a game you can feed your entire game script and only build the characters the game need) - ImVector ranges; - ImFontAtlas::GlyphRangesBuilder builder; - builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) - builder.AddChar(0x7262); // Add a specific character - builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges - builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, ranges.Data); - - All your strings needs to use UTF-8 encoding. In C++11 you can encode a string literal in UTF-8 by using the u8"hello" syntax. - Specifying literal in your source code using a local code page (such as CP-923 for Japanese or CP-1251 for Cyrillic) will NOT work! - Otherwise you can convert yourself to UTF-8 or load text data from file already saved as UTF-8. - - Text input: it is up to your application to pass the right character code to io.AddInputCharacter(). The applications in examples/ are doing that. - For languages using IME, on Windows you can copy the Hwnd of your application to io.ImeWindowHandle. - The default implementation of io.ImeSetInputScreenPosFn() on Windows will set your IME position correctly. - - Q: How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) - A: Create your own context 'ctx = CreateContext()' + 'SetCurrentContext(ctx)' and your own font atlas 'ctx->GetIO().Fonts = new ImFontAtlas()' - so you don't rely on the default globals. - - Q: How can I use the drawing facilities without an ImGui window? (using ImDrawList API) - A: - You can create a dummy window. Call Begin() with NoTitleBar|NoResize|NoMove|NoScrollbar|NoSavedSettings|NoInputs flag, - push a ImGuiCol_WindowBg with zero alpha, then retrieve the ImDrawList* via GetWindowDrawList() and draw to it in any way you like. - - You can call ImGui::GetOverlayDrawList() and use this draw list to display contents over every other imgui windows. - - You can create your own ImDrawList instance. You'll need to initialize them ImGui::GetDrawListSharedData(), or create your own ImDrawListSharedData. - - Q: I integrated Dear ImGui in my engine and the text or lines are blurry.. - A: In your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f). - Also make sure your orthographic projection matrix and io.DisplaySize matches your actual framebuffer dimension. - - Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. - A: You are probably mishandling the clipping rectangles in your render function. - Rectangles provided by ImGui are defined as (x1=left,y1=top,x2=right,y2=bottom) and NOT as (x1,y1,width,height). - - - - tip: you can call Begin() multiple times with the same name during the same frame, it will keep appending to the same window. - this is also useful to set yourself in the context of another window (to get/set other settings) - - tip: you can create widgets without a Begin()/End() block, they will go in an implicit window called "Debug". - - tip: the ImGuiOnceUponAFrame helper will allow run the block of code only once a frame. You can use it to quickly add custom UI in the middle - of a deep nested inner loop in your code. - - tip: you can call Render() multiple times (e.g for VR renders). - - tip: call and read the ShowDemoWindow() code in imgui_demo.cpp for more example of how to use ImGui! + Q: How can I help? + A: - Businesses: please reach out to "omar AT dearimgui DOT com" if you work in a place using Dear ImGui! + We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. + This is among the most useful thing you can do for Dear ImGui. With increased funding, we sustain and grow work on this project. + >>> See https://github.com/ocornut/imgui/wiki/Funding + - Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, and see how you want to help and can help! + - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads. Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui helps the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on GitHub or privately). */ +//------------------------------------------------------------------------- +// [SECTION] INCLUDES +//------------------------------------------------------------------------- + #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -# define _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS #endif -#include "imgui.h" +#ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_internal.h" -#include // toupper, isprint -#include // vsnprintf, sscanf, printf -#include // NULL, malloc, free, qsort, atoi -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -# include // intptr_t +// System includes +#include // vsnprintf, sscanf, printf +#include // intptr_t + +// [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#endif + +// [Windows] OS specific includes (optional) +#if defined(_WIN32) && defined(IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) && defined(IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_FUNCTIONS +#endif +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef __MINGW32__ +#include // _wfopen, OpenClipboard #else -# include // intptr_t +#include +#endif +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP || WINAPI_FAMILY == WINAPI_FAMILY_GAMES) +// The UWP and GDK Win32 API subsets don't support clipboard nor IME functions +#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif #endif -#define IMGUI_DEBUG_NAV_SCORING 0 -#define IMGUI_DEBUG_NAV_RECTS 0 +// [Apple] OS specific includes +#if defined(__APPLE__) +#include +#endif // Visual Studio warnings #ifdef _MSC_VER -# pragma warning(disable : 4127) // condition expression is constant -# pragma warning(disable : 4505) // unreferenced local function has been removed (stb stuff) -# pragma warning(disable : 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types #endif - -// Clang warnings with -Weverything -#ifdef __clang__ -# pragma clang diagnostic ignored "-Wunknown-pragmas" // warning : unknown warning group '-Wformat-pedantic *' // not all warnings are known by all clang versions.. so ignoring warnings triggers new warnings on some configuration. great! -# pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -# pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. -# pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. -# pragma clang diagnostic ignored "-Wexit-time-destructors" // warning : declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. -# pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference it. -# pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // -# pragma clang diagnostic ignored "-Wformat-pedantic" // warning : format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. -# pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning : cast to 'void *' from smaller integer type 'int' // -#elif defined(__GNUC__) -# pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used -# pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size -# pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' -# pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -# pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value -# pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'xxxx' to type 'xxxx' casts away qualifiers -# pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked -# pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to an 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). #endif -// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall -#ifdef _MSC_VER -# define IMGUI_CDECL __cdecl -#else -# define IMGUI_CDECL +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning: format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type 'int' +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#elif defined(__GNUC__) +// We disable -Wpragmas because GCC doesn't provide a has_warning equivalent and some forks/patches may not follow the warning/version association. +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead #endif -//------------------------------------------------------------------------- -// Forward Declarations -//------------------------------------------------------------------------- +// Debug options +#define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Hold CTRL to display for all candidates. CTRL+Arrow to change last direction. +#define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window -static bool IsKeyPressedMap(ImGuiKey key, bool repeat = true); +// When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch. +static const float NAV_WINDOWING_HIGHLIGHT_DELAY = 0.20f; // Time before the highlight and screen dimming starts fading in +static const float NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15f; // Time before the window list starts to appear -static ImFont *GetDefaultFont(); -static void SetCurrentWindow(ImGuiWindow *window); -static void SetWindowScrollX(ImGuiWindow *window, float new_scroll_x); -static void SetWindowScrollY(ImGuiWindow *window, float new_scroll_y); -static void SetWindowPos(ImGuiWindow *window, const ImVec2 &pos, ImGuiCond cond); -static void SetWindowSize(ImGuiWindow *window, const ImVec2 &size, ImGuiCond cond); -static void SetWindowCollapsed(ImGuiWindow *window, bool collapsed, ImGuiCond cond); -static ImGuiWindow *FindHoveredWindow(); -static ImGuiWindow *CreateNewWindow(const char *name, ImVec2 size, ImGuiWindowFlags flags); -static void CheckStacksSize(ImGuiWindow *window, bool write); -static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow *window); +static const float NAV_ACTIVATE_HIGHLIGHT_TIMER = 0.10f; // Time to highlight an item activated by a shortcut. -static void AddDrawListToDrawData(ImVector *out_list, ImDrawList *draw_list); -static void AddWindowToDrawData(ImVector *out_list, ImGuiWindow *window); -static void AddWindowToSortedBuffer(ImVector *out_sorted_windows, ImGuiWindow *window); +// Window resizing from edges (when io.ConfigWindowsResizeFromEdges = true and ImGuiBackendFlags_HasMouseCursors is set in io.BackendFlags by backend) +static const float WINDOWS_HOVER_PADDING = 4.0f; // Extend outside window for hovering/resizing (maxxed with TouchPadding) and inside windows for borders. Affect FindHoveredWindow(). +static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. +static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 0.70f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved. -static ImGuiWindowSettings *AddWindowSettings(const char *name); +// Tooltip offset +static const ImVec2 TOOLTIP_DEFAULT_OFFSET = ImVec2(16, 10); // Multiplied by g.Style.MouseCursorScale -static void LoadIniSettingsFromDisk(const char *ini_filename); -static void LoadIniSettingsFromMemory(const char *buf); -static void SaveIniSettingsToDisk(const char *ini_filename); -static void SaveIniSettingsToMemory(ImVector &out_buf); -static void MarkIniSettingsDirty(ImGuiWindow *window); +//------------------------------------------------------------------------- +// [SECTION] FORWARD DECLARATIONS +//------------------------------------------------------------------------- -static ImRect GetViewportRect(); +static void SetCurrentWindow(ImGuiWindow* window); +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); -static void ClosePopupToLevel(int remaining); -static ImGuiWindow *GetFrontMostModalRootWindow(); +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); -static bool InputTextFilterCharacter(unsigned int *p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void *user_data); -static int InputTextCalcTextLenAndLineCount(const char *text_begin, const char **out_text_end); -static ImVec2 InputTextCalcTextSizeW(const ImWchar *text_begin, const ImWchar *text_end, const ImWchar **remaining = NULL, ImVec2 *out_offset = NULL, bool stop_on_new_line = false); +// Settings +static void WindowSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); +static void WindowSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); +static void WindowSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler*, ImGuiTextBuffer* buf); -static inline void DataTypeFormatString(ImGuiDataType data_type, void *data_ptr, const char *display_format, char *buf, int buf_size); -static inline void DataTypeFormatString(ImGuiDataType data_type, void *data_ptr, int decimal_precision, char *buf, int buf_size); -static void DataTypeApplyOp(ImGuiDataType data_type, int op, void *value1, const void *value2); -static bool DataTypeApplyOpFromText(const char *buf, const char *initial_value_buf, ImGuiDataType data_type, void *data_ptr, const char *scalar_format); +// Platform Dependents default implementation for ImGuiPlatformIO functions +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx); +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text); +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext* ctx, const char* path); namespace ImGui { -static void NavUpdate(); -static void NavUpdateWindowing(); -static void NavProcessItem(ImGuiWindow *window, const ImRect &nav_bb, const ImGuiID id); - -static void UpdateMovingWindow(); -static void UpdateManualResize(ImGuiWindow *window, const ImVec2 &size_auto_fit, int *border_held, int resize_grip_count, ImU32 resize_grip_col[4]); -static void FocusFrontMostActiveWindow(ImGuiWindow *ignore_window); -} // namespace ImGui - -//----------------------------------------------------------------------------- -// Platform dependent default implementations -//----------------------------------------------------------------------------- +// Item +static void ItemHandleShortcut(ImGuiID id); + +// Navigation +static void NavUpdate(); +static void NavUpdateWindowing(); +static void NavUpdateWindowingOverlay(); +static void NavUpdateCancelRequest(); +static void NavUpdateCreateMoveRequest(); +static void NavUpdateCreateTabbingRequest(); +static float NavUpdatePageUpPageDown(); +static inline void NavUpdateAnyRequestFlag(); +static void NavUpdateCreateWrappingRequest(); +static void NavEndFrame(); +static bool NavScoreItem(ImGuiNavItemData* result); +static void NavApplyItemToResult(ImGuiNavItemData* result); +static void NavProcessItem(); +static void NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags); +static ImVec2 NavCalcPreferredRefPos(); +static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window); +static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window); +static void NavRestoreLayer(ImGuiNavLayer layer); +static int FindWindowFocusIndex(ImGuiWindow* window); + +// Error Checking and Debug Tools +static void ErrorCheckNewFrameSanityChecks(); +static void ErrorCheckEndFrameSanityChecks(); +static void UpdateDebugToolItemPicker(); +static void UpdateDebugToolStackQueries(); +static void UpdateDebugToolFlashStyleColor(); + +// Inputs +static void UpdateKeyboardInputs(); +static void UpdateMouseInputs(); +static void UpdateMouseWheel(); +static void UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt); + +// Misc +static void UpdateSettings(); +static int UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); +static void RenderWindowOuterBorders(ImGuiWindow* window); +static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); +static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); +static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col); +static void RenderDimmedBackgrounds(); +static void SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect); + +// Viewports +const ImGuiID IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111; // Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter. +static void UpdateViewportsNewFrame(); -static const char *GetClipboardTextFn_DefaultImpl(void *user_data); -static void SetClipboardTextFn_DefaultImpl(void *user_data, const char *text); -static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y); +} //----------------------------------------------------------------------------- -// Context +// [SECTION] CONTEXT AND MEMORY ALLOCATORS //----------------------------------------------------------------------------- -// Current context pointer. Implicitely used by all ImGui functions. Always assumed to be != NULL. -// CreateContext() will automatically set this pointer if it is NULL. Change to a different context by calling ImGui::SetCurrentContext(). -// If you use DLL hotreloading you might need to call SetCurrentContext() after reloading code from this file. -// ImGui functions are not thread-safe because of this pointer. If you want thread-safety to allow N threads to access N different contexts, you can: -// - Change this variable to use thread local storage. You may #define GImGui in imconfig.h for that purpose. Future development aim to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 -// - Having multiple instances of the ImGui code compiled inside different namespace (easiest/safest, if you have a finite number of contexts) +// DLL users: +// - Heaps and globals are not shared across DLL boundaries! +// - You will need to call SetCurrentContext() + SetAllocatorFunctions() for each static/DLL boundary you are calling from. +// - Same applies for hot-reloading mechanisms that are reliant on reloading DLL (note that many hot-reloading mechanisms work without DLL). +// - Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Confused? In a debugger: add GImGui to your watch window and notice how its value changes depending on your current location (which DLL boundary you are in). + +// Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be != NULL. +// - ImGui::CreateContext() will automatically set this pointer if it is NULL. +// Change to a different context by calling ImGui::SetCurrentContext(). +// - Important: Dear ImGui functions are not thread-safe because of this pointer. +// If you want thread-safety to allow N threads to access N different contexts: +// - Change this variable to use thread local storage so each thread can refer to a different context, in your imconfig.h: +// struct ImGuiContext; +// extern thread_local ImGuiContext* MyImGuiTLS; +// #define GImGui MyImGuiTLS +// And then define MyImGuiTLS in one of your cpp files. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword. +// - Future development aims to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 +// - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from a different namespace. +// - DLL users: read comments above. #ifndef GImGui -ImGuiContext *GImGui = NULL; +ImGuiContext* GImGui = NULL; #endif // Memory Allocator functions. Use SetAllocatorFunctions() to change them. -// If you use DLL hotreloading you might need to call SetAllocatorFunctions() after reloading code from this file. -// Otherwise, you probably don't want to modify them mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - You probably don't want to modify that mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - DLL users: read comments above. #ifndef IMGUI_DISABLE_DEFAULT_ALLOCATORS -static void *MallocWrapper(size_t size, void *user_data) -{ - (void) user_data; - return malloc(size); -} -static void FreeWrapper(void *ptr, void *user_data) -{ - (void) user_data; - free(ptr); -} +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); return malloc(size); } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); free(ptr); } #else -static void *MallocWrapper(size_t size, void *user_data) -{ - (void) user_data; - (void) size; - IM_ASSERT(0); - return NULL; -} -static void FreeWrapper(void *ptr, void *user_data) -{ - (void) user_data; - (void) ptr; - IM_ASSERT(0); -} +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(size); IM_ASSERT(0); return NULL; } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(ptr); IM_ASSERT(0); } #endif - -static void *(*GImAllocatorAllocFunc)(size_t size, void *user_data) = MallocWrapper; -static void (*GImAllocatorFreeFunc)(void *ptr, void *user_data) = FreeWrapper; -static void *GImAllocatorUserData = NULL; -static size_t GImAllocatorActiveAllocationsCount = 0; +static ImGuiMemAllocFunc GImAllocatorAllocFunc = MallocWrapper; +static ImGuiMemFreeFunc GImAllocatorFreeFunc = FreeWrapper; +static void* GImAllocatorUserData = NULL; //----------------------------------------------------------------------------- -// User facing structures +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) //----------------------------------------------------------------------------- ImGuiStyle::ImGuiStyle() { - Alpha = 1.0f; // Global alpha applies to everything in ImGui - WindowPadding = ImVec2(8, 8); // Padding within a window - WindowRounding = 7.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows - WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. - WindowMinSize = ImVec2(32, 32); // Minimum window size - WindowTitleAlign = ImVec2(0.0f, 0.5f); // Alignment for title bar text - ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows - ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. - PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows - PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. - FramePadding = ImVec2(4, 3); // Padding within a framed rectangle (used by most widgets) - FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). - FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. - ItemSpacing = ImVec2(8, 4); // Horizontal and vertical spacing between widgets/lines - ItemInnerSpacing = ImVec2(4, 4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) - TouchExtraPadding = ImVec2(0, 0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! - IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). - ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns - ScrollbarSize = 16.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar - ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar - GrabMinSize = 10.0f; // Minimum width/height of a grab box for slider/scrollbar - GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. - ButtonTextAlign = ImVec2(0.5f, 0.5f); // Alignment of button text when button is larger than text. - DisplayWindowPadding = ImVec2(22, 22); // Window positions are clamped to be visible within the display area by at least this amount. Only covers regular windows. - DisplaySafeAreaPadding = ImVec2(4, 4); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. - MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. - AntiAliasedLines = true; // Enable anti-aliasing on lines/borders. Disable if you are really short on CPU/GPU. - AntiAliasedFill = true; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) - CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. - - ImGui::StyleColorsClassic(this); + Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. + DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + WindowMenuButtonPosition = ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 12.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + TabBorderSize = 0.0f; // Thickness of border around tabs. + TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + TabBarOverlineSize = 2.0f; // Thickness of tab-bar overline, which highlights the selected tab-bar. + TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). + TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell + ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + SeparatorTextBorderSize = 3.0f; // Thickness of border in SeparatorText() + SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. + AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Behaviors + HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + + // Default theme + ImGui::StyleColorsDark(this); } // To scale your entire UI (e.g. if you want your app to use High DPI or generally be DPI aware) you may use this helper function. Scaling the fonts is done separately and is up to you. // Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. void ImGuiStyle::ScaleAllSizes(float scale_factor) { - WindowPadding = ImFloor(WindowPadding * scale_factor); - WindowRounding = ImFloor(WindowRounding * scale_factor); - WindowMinSize = ImFloor(WindowMinSize * scale_factor); - ChildRounding = ImFloor(ChildRounding * scale_factor); - PopupRounding = ImFloor(PopupRounding * scale_factor); - FramePadding = ImFloor(FramePadding * scale_factor); - FrameRounding = ImFloor(FrameRounding * scale_factor); - ItemSpacing = ImFloor(ItemSpacing * scale_factor); - ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); - TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); - IndentSpacing = ImFloor(IndentSpacing * scale_factor); - ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); - ScrollbarSize = ImFloor(ScrollbarSize * scale_factor); - ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor); - GrabMinSize = ImFloor(GrabMinSize * scale_factor); - GrabRounding = ImFloor(GrabRounding * scale_factor); - DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); - DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); - MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); + WindowPadding = ImTrunc(WindowPadding * scale_factor); + WindowRounding = ImTrunc(WindowRounding * scale_factor); + WindowMinSize = ImTrunc(WindowMinSize * scale_factor); + ChildRounding = ImTrunc(ChildRounding * scale_factor); + PopupRounding = ImTrunc(PopupRounding * scale_factor); + FramePadding = ImTrunc(FramePadding * scale_factor); + FrameRounding = ImTrunc(FrameRounding * scale_factor); + ItemSpacing = ImTrunc(ItemSpacing * scale_factor); + ItemInnerSpacing = ImTrunc(ItemInnerSpacing * scale_factor); + CellPadding = ImTrunc(CellPadding * scale_factor); + TouchExtraPadding = ImTrunc(TouchExtraPadding * scale_factor); + IndentSpacing = ImTrunc(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImTrunc(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImTrunc(ScrollbarSize * scale_factor); + ScrollbarRounding = ImTrunc(ScrollbarRounding * scale_factor); + GrabMinSize = ImTrunc(GrabMinSize * scale_factor); + GrabRounding = ImTrunc(GrabRounding * scale_factor); + LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); + TabRounding = ImTrunc(TabRounding * scale_factor); + TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImTrunc(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; + TabBarOverlineSize = ImTrunc(TabBarOverlineSize * scale_factor); + SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); + DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImTrunc(MouseCursorScale * scale_factor); } ImGuiIO::ImGuiIO() { - // Most fields are initialized with zero - memset(this, 0, sizeof(*this)); - - // Settings - DisplaySize = ImVec2(-1.0f, -1.0f); - DeltaTime = 1.0f / 60.0f; - NavFlags = 0x00; - IniSavingRate = 5.0f; - IniFilename = "imgui.ini"; - LogFilename = "imgui_log.txt"; - MouseDoubleClickTime = 0.30f; - MouseDoubleClickMaxDist = 6.0f; - for (int i = 0; i < ImGuiKey_COUNT; i++) - KeyMap[i] = -1; - KeyRepeatDelay = 0.250f; - KeyRepeatRate = 0.050f; - UserData = NULL; - - Fonts = NULL; - FontGlobalScale = 1.0f; - FontDefault = NULL; - FontAllowUserScaling = false; - DisplayFramebufferScale = ImVec2(1.0f, 1.0f); - DisplayVisibleMin = DisplayVisibleMax = ImVec2(0.0f, 0.0f); - - // Advanced/subtle behaviors -#ifdef __APPLE__ - OptMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag -#else - OptMacOSXBehaviors = false; + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + IM_STATIC_ASSERT(IM_ARRAYSIZE(ImGuiIO::MouseDown) == ImGuiMouseButton_COUNT && IM_ARRAYSIZE(ImGuiIO::MouseClicked) == ImGuiMouseButton_COUNT); + + // Settings + ConfigFlags = ImGuiConfigFlags_None; + BackendFlags = ImGuiBackendFlags_None; + DisplaySize = ImVec2(-1.0f, -1.0f); + DeltaTime = 1.0f / 60.0f; + IniSavingRate = 5.0f; + IniFilename = "imgui.ini"; // Important: "imgui.ini" is relative to current working dir, most apps will want to lock this to an absolute path (e.g. same path as executables). + LogFilename = "imgui_log.txt"; +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + for (int i = 0; i < ImGuiKey_COUNT; i++) + KeyMap[i] = -1; #endif - OptCursorBlink = true; + UserData = NULL; - // Settings (User Functions) - GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations - SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; - ClipboardUserData = NULL; - ImeSetInputScreenPosFn = ImeSetInputScreenPosFn_DefaultImpl; - ImeWindowHandle = NULL; + Fonts = NULL; + FontGlobalScale = 1.0f; + FontDefault = NULL; + FontAllowUserScaling = false; + DisplayFramebufferScale = ImVec2(1.0f, 1.0f); -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - RenderDrawListsFn = NULL; + // Miscellaneous options + MouseDrawCursor = false; +#ifdef __APPLE__ + ConfigMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag +#else + ConfigMacOSXBehaviors = false; #endif - - // Input (NB: we already have memset zero the entire structure) - MousePos = ImVec2(-FLT_MAX, -FLT_MAX); - MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); - MouseDragThreshold = 6.0f; - for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) - MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; - for (int i = 0; i < IM_ARRAYSIZE(KeysDownDuration); i++) - KeysDownDuration[i] = KeysDownDurationPrev[i] = -1.0f; - for (int i = 0; i < IM_ARRAYSIZE(NavInputsDownDuration); i++) - NavInputsDownDuration[i] = -1.0f; + ConfigNavSwapGamepadButtons = false; + ConfigInputTrickleEventQueue = true; + ConfigInputTextCursorBlink = true; + ConfigInputTextEnterKeepActive = false; + ConfigDragClickToInputText = false; + ConfigWindowsResizeFromEdges = true; + ConfigWindowsMoveFromTitleBarOnly = false; + ConfigMemoryCompactTimer = 60.0f; + ConfigDebugBeginReturnValueOnce = false; + ConfigDebugBeginReturnValueLoop = false; + + // Inputs Behaviors + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + MouseDragThreshold = 6.0f; + KeyRepeatDelay = 0.275f; + KeyRepeatRate = 0.050f; + + // Platform Functions + // Note: Initialize() will setup default clipboard/ime handlers. + BackendPlatformName = BackendRendererName = NULL; + BackendPlatformUserData = BackendRendererUserData = BackendLanguageUserData = NULL; + + // Input (NB: we already have memset zero the entire structure!) + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); + MouseSource = ImGuiMouseSource_Mouse; + for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(KeysData); i++) { KeysData[i].DownDuration = KeysData[i].DownDurationPrev = -1.0f; } + AppAcceptingEvents = true; + BackendUsingLegacyKeyArrays = (ImS8)-1; + BackendUsingLegacyNavInputArray = true; // assume using legacy array until proven wrong } // Pass in translated ASCII characters for text input. // - with glfw you can get those from the callback set in glfwSetCharCallback() // - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message -void ImGuiIO::AddInputCharacter(ImWchar c) +// FIXME: Should in theory be called "AddCharacterEvent()" to be consistent with new API +void ImGuiIO::AddInputCharacter(unsigned int c) { - const int n = ImStrlenW(InputCharacters); - if (n + 1 < IM_ARRAYSIZE(InputCharacters)) - { - InputCharacters[n] = c; - InputCharacters[n + 1] = '\0'; - } -} + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (c == 0 || !AppAcceptingEvents) + return; -void ImGuiIO::AddInputCharactersUTF8(const char *utf8_chars) -{ - // We can't pass more wchars than ImGuiIO::InputCharacters[] can hold so don't convert more - const int wchars_buf_len = sizeof(ImGuiIO::InputCharacters) / sizeof(ImWchar); - ImWchar wchars[wchars_buf_len]; - ImTextStrFromUtf8(wchars, wchars_buf_len, utf8_chars, NULL); - for (int i = 0; i < wchars_buf_len && wchars[i] != 0; i++) - AddInputCharacter(wchars[i]); + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Text; + e.Source = ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Text.Char = c; + g.InputEventsQueue.push_back(e); } -//----------------------------------------------------------------------------- -// HELPERS -//----------------------------------------------------------------------------- +// UTF16 strings use surrogate pairs to encode codepoints >= 0x10000, so +// we should save the high surrogate. +void ImGuiIO::AddInputCharacterUTF16(ImWchar16 c) +{ + if ((c == 0 && InputQueueSurrogate == 0) || !AppAcceptingEvents) + return; -#define IM_F32_TO_INT8_UNBOUND(_VAL) ((int) ((_VAL) * 255.0f + ((_VAL) >= 0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose -#define IM_F32_TO_INT8_SAT(_VAL) ((int) (ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 + if ((c & 0xFC00) == 0xD800) // High surrogate, must save + { + if (InputQueueSurrogate != 0) + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + InputQueueSurrogate = c; + return; + } -// Play it nice with Windows users. Notepad in 2015 still doesn't display text data with Unix-style \n. -#ifdef _WIN32 -# define IM_NEWLINE "\r\n" + ImWchar cp = c; + if (InputQueueSurrogate != 0) + { + if ((c & 0xFC00) != 0xDC00) // Invalid low surrogate + { + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + } + else + { +#if IM_UNICODE_CODEPOINT_MAX == 0xFFFF + cp = IM_UNICODE_CODEPOINT_INVALID; // Codepoint will not fit in ImWchar #else -# define IM_NEWLINE "\n" + cp = (ImWchar)(((InputQueueSurrogate - 0xD800) << 10) + (c - 0xDC00) + 0x10000); #endif + } -ImVec2 ImLineClosestPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &p) -{ - ImVec2 ap = p - a; - ImVec2 ab_dir = b - a; - float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; - if (dot < 0.0f) - return a; - float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; - if (dot > ab_len_sqr) - return b; - return a + ab_dir * dot / ab_len_sqr; + InputQueueSurrogate = 0; + } + AddInputCharacter((unsigned)cp); } -bool ImTriangleContainsPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p) +void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) { - bool b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0f; - bool b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0f; - bool b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0f; - return ((b1 == b2) && (b2 == b3)); + if (!AppAcceptingEvents) + return; + while (*utf8_chars != 0) + { + unsigned int c = 0; + utf8_chars += ImTextCharFromUtf8(&c, utf8_chars, NULL); + AddInputCharacter(c); + } } -void ImTriangleBarycentricCoords(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p, float &out_u, float &out_v, float &out_w) +// Clear all incoming events. +void ImGuiIO::ClearEventsQueue() { - ImVec2 v0 = b - a; - ImVec2 v1 = c - a; - ImVec2 v2 = p - a; - const float denom = v0.x * v1.y - v1.x * v0.y; - out_v = (v2.x * v1.y - v1.x * v2.y) / denom; - out_w = (v0.x * v2.y - v2.x * v0.y) / denom; - out_u = 1.0f - out_v - out_w; + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsQueue.clear(); } -ImVec2 ImTriangleClosestPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p) +// Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. +void ImGuiIO::ClearInputKeys() { - ImVec2 proj_ab = ImLineClosestPoint(a, b, p); - ImVec2 proj_bc = ImLineClosestPoint(b, c, p); - ImVec2 proj_ca = ImLineClosestPoint(c, a, p); - float dist2_ab = ImLengthSqr(p - proj_ab); - float dist2_bc = ImLengthSqr(p - proj_bc); - float dist2_ca = ImLengthSqr(p - proj_ca); - float m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca)); - if (m == dist2_ab) - return proj_ab; - if (m == dist2_bc) - return proj_bc; - return proj_ca; +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + memset(KeysDown, 0, sizeof(KeysDown)); +#endif + for (int n = 0; n < IM_ARRAYSIZE(KeysData); n++) + { + if (ImGui::IsMouseKey((ImGuiKey)(n + ImGuiKey_KeysData_OFFSET))) + continue; + KeysData[n].Down = false; + KeysData[n].DownDuration = -1.0f; + KeysData[n].DownDurationPrev = -1.0f; + } + KeyCtrl = KeyShift = KeyAlt = KeySuper = false; + KeyMods = ImGuiMod_None; + InputQueueCharacters.resize(0); // Behavior of old ClearInputCharacters(). } -int ImStricmp(const char *str1, const char *str2) +void ImGuiIO::ClearInputMouse() { - int d; - while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) - { - str1++; - str2++; - } - return d; + for (ImGuiKey key = ImGuiKey_Mouse_BEGIN; key < ImGuiKey_Mouse_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &KeysData[key - ImGuiKey_KeysData_OFFSET]; + key_data->Down = false; + key_data->DownDuration = -1.0f; + key_data->DownDurationPrev = -1.0f; + } + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + for (int n = 0; n < IM_ARRAYSIZE(MouseDown); n++) + { + MouseDown[n] = false; + MouseDownDuration[n] = MouseDownDurationPrev[n] = -1.0f; + } + MouseWheel = MouseWheelH = 0.0f; } -int ImStrnicmp(const char *str1, const char *str2, size_t count) +// Removed this as it is ambiguous/misleading and generally incorrect to use with the existence of a higher-level input queue. +// Current frame character buffer is now also cleared by ClearInputKeys(). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +void ImGuiIO::ClearInputCharacters() { - int d = 0; - while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) - { - str1++; - str2++; - count--; - } - return d; + InputQueueCharacters.resize(0); } +#endif -void ImStrncpy(char *dst, const char *src, size_t count) +static ImGuiInputEvent* FindLatestInputEvent(ImGuiContext* ctx, ImGuiInputEventType type, int arg = -1) { - if (count < 1) - return; - strncpy(dst, src, count); - dst[count - 1] = 0; -} + ImGuiContext& g = *ctx; + for (int n = g.InputEventsQueue.Size - 1; n >= 0; n--) + { + ImGuiInputEvent* e = &g.InputEventsQueue[n]; + if (e->Type != type) + continue; + if (type == ImGuiInputEventType_Key && e->Key.Key != arg) + continue; + if (type == ImGuiInputEventType_MouseButton && e->MouseButton.Button != arg) + continue; + return e; + } + return NULL; +} + +// Queue a new key down/up event. +// - ImGuiKey key: Translated key (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) +// - bool down: Is the key down? use false to signify a key release. +// - float analog_value: 0.0f..1.0f +// IMPORTANT: THIS FUNCTION AND OTHER "ADD" GRABS THE CONTEXT FROM OUR INSTANCE. +// WE NEED TO ENSURE THAT ALL FUNCTION CALLS ARE FULFILLING THIS, WHICH IS WHY GetKeyData() HAS AN EXPLICIT CONTEXT. +void ImGuiIO::AddKeyAnalogEvent(ImGuiKey key, bool down, float analog_value) +{ + //if (e->Down) { IMGUI_DEBUG_LOG_IO("AddKeyEvent() Key='%s' %d, NativeKeycode = %d, NativeScancode = %d\n", ImGui::GetKeyName(e->Key), e->Down, e->NativeKeycode, e->NativeScancode); } + IM_ASSERT(Ctx != NULL); + if (key == ImGuiKey_None || !AppAcceptingEvents) + return; + ImGuiContext& g = *Ctx; + IM_ASSERT(ImGui::IsNamedKeyOrMod(key)); // Backend needs to pass a valid ImGuiKey_ constant. 0..511 values are legacy native key codes which are not accepted by this API. + IM_ASSERT(ImGui::IsAliasKey(key) == false); // Backend cannot submit ImGuiKey_MouseXXX values they are automatically inferred from AddMouseXXX() events. + + // MacOS: swap Cmd(Super) and Ctrl + if (g.IO.ConfigMacOSXBehaviors) + { + if (key == ImGuiMod_Super) { key = ImGuiMod_Ctrl; } + else if (key == ImGuiMod_Ctrl) { key = ImGuiMod_Super; } + else if (key == ImGuiKey_LeftSuper) { key = ImGuiKey_LeftCtrl; } + else if (key == ImGuiKey_RightSuper){ key = ImGuiKey_RightCtrl; } + else if (key == ImGuiKey_LeftCtrl) { key = ImGuiKey_LeftSuper; } + else if (key == ImGuiKey_RightCtrl) { key = ImGuiKey_RightSuper; } + } -char *ImStrdup(const char *str) -{ - size_t len = strlen(str) + 1; - void *buf = ImGui::MemAlloc(len); - return (char *) memcpy(buf, (const void *) str, len); + // Verify that backend isn't mixing up using new io.AddKeyEvent() api and old io.KeysDown[] + io.KeyMap[] data. +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT((BackendUsingLegacyKeyArrays == -1 || BackendUsingLegacyKeyArrays == 0) && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + if (BackendUsingLegacyKeyArrays == -1) + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_NamedKey_END; n++) + IM_ASSERT(KeyMap[n] == -1 && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + BackendUsingLegacyKeyArrays = 0; +#endif + if (ImGui::IsGamepadKey(key)) + BackendUsingLegacyNavInputArray = false; + + // Filter duplicate (in particular: key mods and gamepad analog values are commonly spammed) + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)key); + const ImGuiKeyData* key_data = ImGui::GetKeyData(&g, key); + const bool latest_key_down = latest_event ? latest_event->Key.Down : key_data->Down; + const float latest_key_analog = latest_event ? latest_event->Key.AnalogValue : key_data->AnalogValue; + if (latest_key_down == down && latest_key_analog == analog_value) + return; + + // Add event + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Key; + e.Source = ImGui::IsGamepadKey(key) ? ImGuiInputSource_Gamepad : ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Key.Key = key; + e.Key.Down = down; + e.Key.AnalogValue = analog_value; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddKeyEvent(ImGuiKey key, bool down) +{ + if (!AppAcceptingEvents) + return; + AddKeyAnalogEvent(key, down, down ? 1.0f : 0.0f); +} + +// [Optional] Call after AddKeyEvent(). +// Specify native keycode, scancode + Specify index for legacy <1.87 IsKeyXXX() functions with native indices. +// If you are writing a backend in 2022 or don't use IsKeyXXX() with native values that are not ImGuiKey values, you can avoid calling this. +void ImGuiIO::SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index) +{ + if (key == ImGuiKey_None) + return; + IM_ASSERT(ImGui::IsNamedKey(key)); // >= 512 + IM_ASSERT(native_legacy_index == -1 || ImGui::IsLegacyKey((ImGuiKey)native_legacy_index)); // >= 0 && <= 511 + IM_UNUSED(native_keycode); // Yet unused + IM_UNUSED(native_scancode); // Yet unused + + // Build native->imgui map so old user code can still call key functions with native 0..511 values. +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + const int legacy_key = (native_legacy_index != -1) ? native_legacy_index : native_keycode; + if (!ImGui::IsLegacyKey((ImGuiKey)legacy_key)) + return; + KeyMap[legacy_key] = key; + KeyMap[key] = legacy_key; +#else + IM_UNUSED(key); + IM_UNUSED(native_legacy_index); +#endif } -char *ImStrchrRange(const char *str, const char *str_end, char c) +// Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. +void ImGuiIO::SetAppAcceptingEvents(bool accepting_events) { - for (; str < str_end; str++) - if (*str == c) - return (char *) str; - return NULL; + AppAcceptingEvents = accepting_events; } -int ImStrlenW(const ImWchar *str) +// Queue a mouse move event +void ImGuiIO::AddMousePosEvent(float x, float y) { - int n = 0; - while (*str++) - n++; - return n; -} + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (!AppAcceptingEvents) + return; -const ImWchar *ImStrbolW(const ImWchar *buf_mid_line, const ImWchar *buf_begin) // find beginning-of-line -{ - while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') - buf_mid_line--; - return buf_mid_line; -} + // Apply same flooring as UpdateMouseInputs() + ImVec2 pos((x > -FLT_MAX) ? ImFloor(x) : x, (y > -FLT_MAX) ? ImFloor(y) : y); -const char *ImStristr(const char *haystack, const char *haystack_end, const char *needle, const char *needle_end) -{ - if (!needle_end) - needle_end = needle + strlen(needle); + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MousePos); + const ImVec2 latest_pos = latest_event ? ImVec2(latest_event->MousePos.PosX, latest_event->MousePos.PosY) : g.IO.MousePos; + if (latest_pos.x == pos.x && latest_pos.y == pos.y) + return; - const char un0 = (char) toupper(*needle); - while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) - { - if (toupper(*haystack) == un0) - { - const char *b = needle + 1; - for (const char *a = haystack + 1; b < needle_end; a++, b++) - if (toupper(*a) != toupper(*b)) - break; - if (b == needle_end) - return haystack; - } - haystack++; - } - return NULL; + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MousePos; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MousePos.PosX = pos.x; + e.MousePos.PosY = pos.y; + e.MousePos.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); } -static const char *ImAtoi(const char *src, int *output) +void ImGuiIO::AddMouseButtonEvent(int mouse_button, bool down) { - int negative = 0; - if (*src == '-') - { - negative = 1; - src++; - } - if (*src == '+') - { - src++; - } - int v = 0; - while (*src >= '0' && *src <= '9') - v = (v * 10) + (*src++ - '0'); - *output = negative ? -v : v; - return src; -} + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + if (!AppAcceptingEvents) + return; -// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). -// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. -// B) When buf==NULL vsnprintf() will return the output size. -#ifndef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS -int ImFormatString(char *buf, size_t buf_size, const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - int w = vsnprintf(buf, buf_size, fmt, args); - va_end(args); - if (buf == NULL) - return w; - if (w == -1 || w >= (int) buf_size) - w = (int) buf_size - 1; - buf[w] = 0; - return w; -} - -int ImFormatStringV(char *buf, size_t buf_size, const char *fmt, va_list args) -{ - int w = vsnprintf(buf, buf_size, fmt, args); - if (buf == NULL) - return w; - if (w == -1 || w >= (int) buf_size) - w = (int) buf_size - 1; - buf[w] = 0; - return w; -} -#endif // #ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS - -// Pass data_size==0 for zero-terminated strings -// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. -ImU32 ImHash(const void *data, int data_size, ImU32 seed) -{ - static ImU32 crc32_lut[256] = {0}; - if (!crc32_lut[1]) - { - const ImU32 polynomial = 0xEDB88320; - for (ImU32 i = 0; i < 256; i++) - { - ImU32 crc = i; - for (ImU32 j = 0; j < 8; j++) - crc = (crc >> 1) ^ (ImU32(-int(crc & 1)) & polynomial); - crc32_lut[i] = crc; - } - } - - seed = ~seed; - ImU32 crc = seed; - const unsigned char *current = (const unsigned char *) data; - - if (data_size > 0) - { - // Known size - while (data_size--) - crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *current++]; - } - else - { - // Zero-terminated string - while (unsigned char c = *current++) - { - // We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. - // Because this syntax is rarely used we are optimizing for the common case. - // - If we reach ### in the string we discard the hash so far and reset to the seed. - // - We don't do 'current += 2; continue;' after handling ### to keep the code smaller. - if (c == '#' && current[0] == '#' && current[1] == '#') - crc = seed; - crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; - } - } - return ~crc; -} + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click: handle held button. + if (ConfigMacOSXBehaviors && mouse_button == 0 && MouseCtrlLeftAsRightClick) + { + // Order of both statements matterns: this event will still release mouse button 1 + mouse_button = 1; + if (!down) + MouseCtrlLeftAsRightClick = false; + } -//----------------------------------------------------------------------------- -// ImText* helpers -//----------------------------------------------------------------------------- + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MouseButton, (int)mouse_button); + const bool latest_button_down = latest_event ? latest_event->MouseButton.Down : g.IO.MouseDown[mouse_button]; + if (latest_button_down == down) + return; -// Convert UTF-8 to 32-bits character, process single character input. -// Based on stb_from_utf8() from github.com/nothings/stb/ -// We handle UTF-8 decoding error by skipping forward. -int ImTextCharFromUtf8(unsigned int *out_char, const char *in_text, const char *in_text_end) -{ - unsigned int c = (unsigned int) -1; - const unsigned char *str = (const unsigned char *) in_text; - if (!(*str & 0x80)) - { - c = (unsigned int) (*str++); - *out_char = c; - return 1; - } - if ((*str & 0xe0) == 0xc0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char *) str < 2) - return 1; - if (*str < 0xc2) - return 2; - c = (unsigned int) ((*str++ & 0x1f) << 6); - if ((*str & 0xc0) != 0x80) - return 2; - c += (*str++ & 0x3f); - *out_char = c; - return 2; - } - if ((*str & 0xf0) == 0xe0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char *) str < 3) - return 1; - if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) - return 3; - if (*str == 0xed && str[1] > 0x9f) - return 3; // str[1] < 0x80 is checked below - c = (unsigned int) ((*str++ & 0x0f) << 12); - if ((*str & 0xc0) != 0x80) - return 3; - c += (unsigned int) ((*str++ & 0x3f) << 6); - if ((*str & 0xc0) != 0x80) - return 3; - c += (*str++ & 0x3f); - *out_char = c; - return 3; - } - if ((*str & 0xf8) == 0xf0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char *) str < 4) - return 1; - if (*str > 0xf4) - return 4; - if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) - return 4; - if (*str == 0xf4 && str[1] > 0x8f) - return 4; // str[1] < 0x80 is checked below - c = (unsigned int) ((*str++ & 0x07) << 18); - if ((*str & 0xc0) != 0x80) - return 4; - c += (unsigned int) ((*str++ & 0x3f) << 12); - if ((*str & 0xc0) != 0x80) - return 4; - c += (unsigned int) ((*str++ & 0x3f) << 6); - if ((*str & 0xc0) != 0x80) - return 4; - c += (*str++ & 0x3f); - // utf-8 encodings of values used in surrogate pairs are invalid - if ((c & 0xFFFFF800) == 0xD800) - return 4; - *out_char = c; - return 4; - } - *out_char = 0; - return 0; -} - -int ImTextStrFromUtf8(ImWchar *buf, int buf_size, const char *in_text, const char *in_text_end, const char **in_text_remaining) -{ - ImWchar *buf_out = buf; - ImWchar *buf_end = buf + buf_size; - while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) - { - unsigned int c; - in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); - if (c == 0) - break; - if (c < 0x10000) // FIXME: Losing characters that don't fit in 2 bytes - *buf_out++ = (ImWchar) c; - } - *buf_out = 0; - if (in_text_remaining) - *in_text_remaining = in_text; - return (int) (buf_out - buf); -} - -int ImTextCountCharsFromUtf8(const char *in_text, const char *in_text_end) -{ - int char_count = 0; - while ((!in_text_end || in_text < in_text_end) && *in_text) - { - unsigned int c; - in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); - if (c == 0) - break; - if (c < 0x10000) - char_count++; - } - return char_count; + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click. + // - Note that this is actual physical Ctrl which is ImGuiMod_Super for us. + // - At this point we want from !down to down, so this is handling the initial press. + if (ConfigMacOSXBehaviors && mouse_button == 0 && down) + { + const ImGuiInputEvent* latest_super_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)ImGuiMod_Super); + if (latest_super_event ? latest_super_event->Key.Down : g.IO.KeySuper) + { + IMGUI_DEBUG_LOG_IO("[io] Super+Left Click aliased into Right Click\n"); + MouseCtrlLeftAsRightClick = true; + AddMouseButtonEvent(1, true); // This is just quicker to write that passing through, as we need to filter duplicate again. + return; + } + } + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseButton; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseButton.Button = mouse_button; + e.MouseButton.Down = down; + e.MouseButton.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); } -// Based on stb_to_utf8() from github.com/nothings/stb/ -static inline int ImTextCharToUtf8(char *buf, int buf_size, unsigned int c) -{ - if (c < 0x80) - { - buf[0] = (char) c; - return 1; - } - if (c < 0x800) - { - if (buf_size < 2) - return 0; - buf[0] = (char) (0xc0 + (c >> 6)); - buf[1] = (char) (0x80 + (c & 0x3f)); - return 2; - } - if (c >= 0xdc00 && c < 0xe000) - { - return 0; - } - if (c >= 0xd800 && c < 0xdc00) - { - if (buf_size < 4) - return 0; - buf[0] = (char) (0xf0 + (c >> 18)); - buf[1] = (char) (0x80 + ((c >> 12) & 0x3f)); - buf[2] = (char) (0x80 + ((c >> 6) & 0x3f)); - buf[3] = (char) (0x80 + ((c) & 0x3f)); - return 4; - } - // else if (c < 0x10000) - { - if (buf_size < 3) - return 0; - buf[0] = (char) (0xe0 + (c >> 12)); - buf[1] = (char) (0x80 + ((c >> 6) & 0x3f)); - buf[2] = (char) (0x80 + ((c) & 0x3f)); - return 3; - } +// Queue a mouse wheel event (some mouse/API may only have a Y component) +void ImGuiIO::AddMouseWheelEvent(float wheel_x, float wheel_y) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate (unlike most events, wheel values are relative and easy to filter) + if (!AppAcceptingEvents || (wheel_x == 0.0f && wheel_y == 0.0f)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseWheel; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseWheel.WheelX = wheel_x; + e.MouseWheel.WheelY = wheel_y; + e.MouseWheel.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); } -static inline int ImTextCountUtf8BytesFromChar(unsigned int c) +// This is not a real event, the data is latched in order to be stored in actual Mouse events. +// This is so that duplicate events (e.g. Windows sending extraneous WM_MOUSEMOVE) gets filtered and are not leading to actual source changes. +void ImGuiIO::AddMouseSourceEvent(ImGuiMouseSource source) { - if (c < 0x80) - return 1; - if (c < 0x800) - return 2; - if (c >= 0xdc00 && c < 0xe000) - return 0; - if (c >= 0xd800 && c < 0xdc00) - return 4; - return 3; -} - -int ImTextStrToUtf8(char *buf, int buf_size, const ImWchar *in_text, const ImWchar *in_text_end) -{ - char *buf_out = buf; - const char *buf_end = buf + buf_size; - while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) - { - unsigned int c = (unsigned int) (*in_text++); - if (c < 0x80) - *buf_out++ = (char) c; - else - buf_out += ImTextCharToUtf8(buf_out, (int) (buf_end - buf_out - 1), c); - } - *buf_out = 0; - return (int) (buf_out - buf); -} - -int ImTextCountUtf8BytesFromStr(const ImWchar *in_text, const ImWchar *in_text_end) -{ - int bytes_count = 0; - while ((!in_text_end || in_text < in_text_end) && *in_text) - { - unsigned int c = (unsigned int) (*in_text++); - if (c < 0x80) - bytes_count++; - else - bytes_count += ImTextCountUtf8BytesFromChar(c); - } - return bytes_count; + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsNextMouseSource = source; } -ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +void ImGuiIO::AddFocusEvent(bool focused) { - float s = 1.0f / 255.0f; - return ImVec4( - ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, - ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, - ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, - ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Focus); + const bool latest_focused = latest_event ? latest_event->AppFocused.Focused : !g.IO.AppFocusLost; + if (latest_focused == focused || (ConfigDebugIgnoreFocusLoss && !focused)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Focus; + e.EventId = g.InputEventsNextEventId++; + e.AppFocused.Focused = focused; + g.InputEventsQueue.push_back(e); } -ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4 &in) +ImGuiPlatformIO::ImGuiPlatformIO() { - ImU32 out; - out = ((ImU32) IM_F32_TO_INT8_SAT(in.x)) << IM_COL32_R_SHIFT; - out |= ((ImU32) IM_F32_TO_INT8_SAT(in.y)) << IM_COL32_G_SHIFT; - out |= ((ImU32) IM_F32_TO_INT8_SAT(in.z)) << IM_COL32_B_SHIFT; - out |= ((ImU32) IM_F32_TO_INT8_SAT(in.w)) << IM_COL32_A_SHIFT; - return out; + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + Platform_LocaleDecimalPoint = '.'; } -ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +//----------------------------------------------------------------------------- + +ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments) { - ImGuiStyle &style = GImGui->Style; - ImVec4 c = style.Colors[idx]; - c.w *= style.Alpha * alpha_mul; - return ColorConvertFloat4ToU32(c); + IM_ASSERT(num_segments > 0); // Use ImBezierCubicClosestPointCasteljau() + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + ImVec2 p_current = ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + return p_closest; } -ImU32 ImGui::GetColorU32(const ImVec4 &col) +// Closely mimics PathBezierToCasteljau() in imgui_draw.cpp +static void ImBezierCubicClosestPointCasteljauStep(const ImVec2& p, ImVec2& p_closest, ImVec2& p_last, float& p_closest_dist2, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) { - ImGuiStyle &style = GImGui->Style; - ImVec4 c = col; - c.w *= style.Alpha; - return ColorConvertFloat4ToU32(c); + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + ImVec2 p_current(x4, y4); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + else if (level < 10) + { + float x12 = (x1 + x2)*0.5f, y12 = (y1 + y2)*0.5f; + float x23 = (x2 + x3)*0.5f, y23 = (y2 + y3)*0.5f; + float x34 = (x3 + x4)*0.5f, y34 = (y3 + y4)*0.5f; + float x123 = (x12 + x23)*0.5f, y123 = (y12 + y23)*0.5f; + float x234 = (x23 + x34)*0.5f, y234 = (y23 + y34)*0.5f; + float x1234 = (x123 + x234)*0.5f, y1234 = (y123 + y234)*0.5f; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } } -const ImVec4 &ImGui::GetStyleColorVec4(ImGuiCol idx) +// tess_tol is generally the same value you would find in ImGui::GetStyle().CurveTessellationTol +// Because those ImXXX functions are lower-level than ImGui:: we cannot access this value automatically. +ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol) { - ImGuiStyle &style = GImGui->Style; - return style.Colors[idx]; + IM_ASSERT(tess_tol > 0.0f); + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, tess_tol, 0); + return p_closest; } -ImU32 ImGui::GetColorU32(ImU32 col) +ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) { - float style_alpha = GImGui->Style.Alpha; - if (style_alpha >= 1.0f) - return col; - int a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; - a = (int) (a * style_alpha); // We don't need to clamp 0..255 because Style.Alpha is in 0..1 range. - return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); + ImVec2 ap = p - a; + ImVec2 ab_dir = b - a; + float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if (dot < 0.0f) + return a; + float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if (dot > ab_len_sqr) + return b; + return a + ab_dir * dot / ab_len_sqr; } -// Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592 -// Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv -void ImGui::ColorConvertRGBtoHSV(float r, float g, float b, float &out_h, float &out_s, float &out_v) -{ - float K = 0.f; - if (g < b) - { - ImSwap(g, b); - K = -1.f; - } - if (r < g) - { - ImSwap(r, g); - K = -2.f / 6.f - K; - } - - const float chroma = r - (g < b ? g : b); - out_h = fabsf(K + (g - b) / (6.f * chroma + 1e-20f)); - out_s = chroma / (r + 1e-20f); - out_v = r; +bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + bool b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0f; + bool b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0f; + bool b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0f; + return ((b1 == b2) && (b2 == b3)); } -// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 -// also http://en.wikipedia.org/wiki/HSL_and_HSV -void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float &out_r, float &out_g, float &out_b) -{ - if (s == 0.0f) - { - // gray - out_r = out_g = out_b = v; - return; - } - - h = fmodf(h, 1.0f) / (60.0f / 360.0f); - int i = (int) h; - float f = h - (float) i; - float p = v * (1.0f - s); - float q = v * (1.0f - s * f); - float t = v * (1.0f - s * (1.0f - f)); - - switch (i) - { - case 0: - out_r = v; - out_g = t; - out_b = p; - break; - case 1: - out_r = q; - out_g = v; - out_b = p; - break; - case 2: - out_r = p; - out_g = v; - out_b = t; - break; - case 3: - out_r = p; - out_g = q; - out_b = v; - break; - case 4: - out_r = t; - out_g = p; - out_b = v; - break; - case 5: - default: - out_r = v; - out_g = p; - out_b = q; - break; - } -} - -FILE *ImFileOpen(const char *filename, const char *mode) -{ -#if defined(_WIN32) && !defined(__CYGWIN__) - // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. Converting both strings from UTF-8 to wchar format (using a single allocation, because we can) - const int filename_wsize = ImTextCountCharsFromUtf8(filename, NULL) + 1; - const int mode_wsize = ImTextCountCharsFromUtf8(mode, NULL) + 1; - ImVector buf; - buf.resize(filename_wsize + mode_wsize); - ImTextStrFromUtf8(&buf[0], filename_wsize, filename, NULL); - ImTextStrFromUtf8(&buf[filename_wsize], mode_wsize, mode, NULL); - return _wfopen((wchar_t *) &buf[0], (wchar_t *) &buf[filename_wsize]); -#else - return fopen(filename, mode); -#endif +void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w) +{ + ImVec2 v0 = b - a; + ImVec2 v1 = c - a; + ImVec2 v2 = p - a; + const float denom = v0.x * v1.y - v1.x * v0.y; + out_v = (v2.x * v1.y - v1.x * v2.y) / denom; + out_w = (v0.x * v2.y - v2.x * v0.y) / denom; + out_u = 1.0f - out_v - out_w; } -// Load file content into memory -// Memory allocated with ImGui::MemAlloc(), must be freed by user using ImGui::MemFree() -void *ImFileLoadToMemory(const char *filename, const char *file_open_mode, int *out_file_size, int padding_bytes) -{ - IM_ASSERT(filename && file_open_mode); - if (out_file_size) - *out_file_size = 0; - - FILE *f; - if ((f = ImFileOpen(filename, file_open_mode)) == NULL) - return NULL; - - long file_size_signed; - if (fseek(f, 0, SEEK_END) || (file_size_signed = ftell(f)) == -1 || fseek(f, 0, SEEK_SET)) - { - fclose(f); - return NULL; - } - - int file_size = (int) file_size_signed; - void *file_data = ImGui::MemAlloc(file_size + padding_bytes); - if (file_data == NULL) - { - fclose(f); - return NULL; - } - if (fread(file_data, 1, (size_t) file_size, f) != (size_t) file_size) - { - fclose(f); - ImGui::MemFree(file_data); - return NULL; - } - if (padding_bytes > 0) - memset((void *) (((char *) file_data) + file_size), 0, padding_bytes); - - fclose(f); - if (out_file_size) - *out_file_size = file_size; - - return file_data; +ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + ImVec2 proj_ab = ImLineClosestPoint(a, b, p); + ImVec2 proj_bc = ImLineClosestPoint(b, c, p); + ImVec2 proj_ca = ImLineClosestPoint(c, a, p); + float dist2_ab = ImLengthSqr(p - proj_ab); + float dist2_bc = ImLengthSqr(p - proj_bc); + float dist2_ca = ImLengthSqr(p - proj_ca); + float m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca)); + if (m == dist2_ab) + return proj_ab; + if (m == dist2_bc) + return proj_bc; + return proj_ca; } //----------------------------------------------------------------------------- -// ImGuiStorage -// Helper: Key->value storage +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) //----------------------------------------------------------------------------- -// std::lower_bound but without the bullshit -static ImVector::iterator LowerBound(ImVector &data, ImGuiID key) -{ - ImVector::iterator first = data.begin(); - ImVector::iterator last = data.end(); - size_t count = (size_t) (last - first); - while (count > 0) - { - size_t count2 = count >> 1; - ImVector::iterator mid = first + count2; - if (mid->key < key) - { - first = ++mid; - count -= count2 + 1; - } - else - { - count = count2; - } - } - return first; +// Consider using _stricmp/_strnicmp under Windows or strcasecmp/strncasecmp. We don't actually use either ImStricmp/ImStrnicmp in the codebase any more. +int ImStricmp(const char* str1, const char* str2) +{ + int d; + while ((d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; } + return d; } -// For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. -void ImGuiStorage::BuildSortByKey() +int ImStrnicmp(const char* str1, const char* str2, size_t count) { - struct StaticFunc - { - static int IMGUI_CDECL PairCompareByID(const void *lhs, const void *rhs) - { - // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. - if (((const Pair *) lhs)->key > ((const Pair *) rhs)->key) - return +1; - if (((const Pair *) lhs)->key < ((const Pair *) rhs)->key) - return -1; - return 0; - } - }; - if (Data.Size > 1) - qsort(Data.Data, (size_t) Data.Size, sizeof(Pair), StaticFunc::PairCompareByID); + int d = 0; + while (count > 0 && (d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; count--; } + return d; } -int ImGuiStorage::GetInt(ImGuiID key, int default_val) const +void ImStrncpy(char* dst, const char* src, size_t count) { - ImVector::iterator it = LowerBound(const_cast &>(Data), key); - if (it == Data.end() || it->key != key) - return default_val; - return it->val_i; + if (count < 1) + return; + if (count > 1) + strncpy(dst, src, count - 1); + dst[count - 1] = 0; } -bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const +char* ImStrdup(const char* str) { - return GetInt(key, default_val ? 1 : 0) != 0; + size_t len = strlen(str); + void* buf = IM_ALLOC(len + 1); + return (char*)memcpy(buf, (const void*)str, len + 1); } -float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const +char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src) { - ImVector::iterator it = LowerBound(const_cast &>(Data), key); - if (it == Data.end() || it->key != key) - return default_val; - return it->val_f; + size_t dst_buf_size = p_dst_size ? *p_dst_size : strlen(dst) + 1; + size_t src_size = strlen(src) + 1; + if (dst_buf_size < src_size) + { + IM_FREE(dst); + dst = (char*)IM_ALLOC(src_size); + if (p_dst_size) + *p_dst_size = src_size; + } + return (char*)memcpy(dst, (const void*)src, src_size); } -void *ImGuiStorage::GetVoidPtr(ImGuiID key) const +const char* ImStrchrRange(const char* str, const char* str_end, char c) { - ImVector::iterator it = LowerBound(const_cast &>(Data), key); - if (it == Data.end() || it->key != key) - return NULL; - return it->val_p; + const char* p = (const char*)memchr(str, (int)c, str_end - str); + return p; } -// References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. -int *ImGuiStorage::GetIntRef(ImGuiID key, int default_val) +int ImStrlenW(const ImWchar* str) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - it = Data.insert(it, Pair(key, default_val)); - return &it->val_i; + //return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bit + int n = 0; + while (*str++) n++; + return n; } -bool *ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) +// Find end-of-line. Return pointer will point to either first \n, either str_end. +const char* ImStreolRange(const char* str, const char* str_end) { - return (bool *) GetIntRef(key, default_val ? 1 : 0); + const char* p = (const char*)memchr(str, '\n', str_end - str); + return p ? p : str_end; } -float *ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) +const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - it = Data.insert(it, Pair(key, default_val)); - return &it->val_f; + while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') + buf_mid_line--; + return buf_mid_line; } -void **ImGuiStorage::GetVoidPtrRef(ImGuiID key, void *default_val) +const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - it = Data.insert(it, Pair(key, default_val)); - return &it->val_p; + if (!needle_end) + needle_end = needle + strlen(needle); + + const char un0 = (char)ImToUpper(*needle); + while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) + { + if (ImToUpper(*haystack) == un0) + { + const char* b = needle + 1; + for (const char* a = haystack + 1; b < needle_end; a++, b++) + if (ImToUpper(*a) != ImToUpper(*b)) + break; + if (b == needle_end) + return haystack; + } + haystack++; + } + return NULL; } -// FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) -void ImGuiStorage::SetInt(ImGuiID key, int val) +// Trim str by offsetting contents when there's leading data + writing a \0 at the trailing position. We use this in situation where the cost is negligible. +void ImStrTrimBlanks(char* buf) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_i = val; + char* p = buf; + while (p[0] == ' ' || p[0] == '\t') // Leading blanks + p++; + char* p_start = p; + while (*p != 0) // Find end of string + p++; + while (p > p_start && (p[-1] == ' ' || p[-1] == '\t')) // Trailing blanks + p--; + if (p_start != buf) // Copy memory if we had leading blanks + memmove(buf, p_start, p - p_start); + buf[p - p_start] = 0; // Zero terminate } -void ImGuiStorage::SetBool(ImGuiID key, bool val) +const char* ImStrSkipBlank(const char* str) { - SetInt(key, val ? 1 : 0); + while (str[0] == ' ' || str[0] == '\t') + str++; + return str; } -void ImGuiStorage::SetFloat(ImGuiID key, float val) +// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). +// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. +// B) When buf==NULL vsnprintf() will return the output size. +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + +// We support stb_sprintf which is much faster (see: https://github.com/nothings/stb/blob/master/stb_sprintf.h) +// You may set IMGUI_USE_STB_SPRINTF to use our default wrapper, or set IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +// and setup the wrapper yourself. (FIXME-OPT: Some of our high-level operations such as ImGuiTextBuffer::appendfv() are +// designed using two-passes worst case, which probably could be improved using the stbsp_vsprintfcb() function.) +#ifdef IMGUI_USE_STB_SPRINTF +#ifndef IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION +#define STB_SPRINTF_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_SPRINTF_FILENAME +#include IMGUI_STB_SPRINTF_FILENAME +#else +#include "stb_sprintf.h" +#endif +#endif // #ifdef IMGUI_USE_STB_SPRINTF + +#if defined(_MSC_VER) && !defined(vsnprintf) +#define vsnprintf _vsnprintf +#endif + +int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_f = val; + va_list args; + va_start(args, fmt); +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; } -void ImGuiStorage::SetVoidPtr(ImGuiID key, void *val) +int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_p = val; +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; } +#endif // #ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS -void ImGuiStorage::SetAllInt(int v) +void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) { - for (int i = 0; i < Data.Size; i++) - Data[i].val_i = v; + va_list args; + va_start(args, fmt); + ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); + va_end(args); } -//----------------------------------------------------------------------------- -// ImGuiTextFilter -//----------------------------------------------------------------------------- +// FIXME: Should rework API toward allowing multiple in-flight temp buffers (easier and safer for caller) +// by making the caller acquire a temp buffer token, with either explicit or destructor release, e.g. +// ImGuiTempBufferToken token; +// ImFormatStringToTempBuffer(token, ...); +void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + { + const char* buf = va_arg(args, const char*); // Skip formatting when using "%s" + if (buf == NULL) + buf = "(null)"; + *out_buf = buf; + if (out_buf_end) { *out_buf_end = buf + strlen(buf); } + } + else if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '*' && fmt[3] == 's' && fmt[4] == 0) + { + int buf_len = va_arg(args, int); // Skip formatting when using "%.*s" + const char* buf = va_arg(args, const char*); + if (buf == NULL) + { + buf = "(null)"; + buf_len = ImMin(buf_len, 6); + } + *out_buf = buf; + *out_buf_end = buf + buf_len; // Disallow not passing 'out_buf_end' here. User is expected to use it. + } + else + { + int buf_len = ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); + *out_buf = g.TempBuffer.Data; + if (out_buf_end) { *out_buf_end = g.TempBuffer.Data + buf_len; } + } +} -// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" -ImGuiTextFilter::ImGuiTextFilter(const char *default_filter) -{ - if (default_filter) - { - ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); - Build(); - } - else - { - InputBuf[0] = 0; - CountGrep = 0; - } -} - -bool ImGuiTextFilter::Draw(const char *label, float width) -{ - if (width != 0.0f) - ImGui::PushItemWidth(width); - bool value_changed = ImGui::InputText(label, InputBuf, IM_ARRAYSIZE(InputBuf)); - if (width != 0.0f) - ImGui::PopItemWidth(); - if (value_changed) - Build(); - return value_changed; -} - -void ImGuiTextFilter::TextRange::split(char separator, ImVector &out) -{ - out.resize(0); - const char *wb = b; - const char *we = wb; - while (we < e) - { - if (*we == separator) - { - out.push_back(TextRange(wb, we)); - wb = we + 1; - } - we++; - } - if (wb != we) - out.push_back(TextRange(wb, we)); +// CRC32 needs a 1KB lookup table (not cache friendly) +// Although the code to generate the table is simple and shorter than the table itself, using a const table allows us to easily: +// - avoid an unnecessary branch/memory tap, - keep the ImHashXXX functions usable by static constructors, - make it thread-safe. +static const ImU32 GCrc32LookupTable[256] = +{ + 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, +}; + +// Known size hash +// It is ok to call ImHashData on a string with known length but the ### operator won't be supported. +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashData(const void* data_p, size_t data_size, ImGuiID seed) +{ + ImU32 crc = ~seed; + const unsigned char* data = (const unsigned char*)data_p; + const ImU32* crc32_lut = GCrc32LookupTable; + while (data_size-- != 0) + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *data++]; + return ~crc; } -void ImGuiTextFilter::Build() +// Zero-terminated string hash, with support for ### to reset back to seed value +// We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. +// Because this syntax is rarely used we are optimizing for the common case. +// - If we reach ### in the string we discard the hash so far and reset to the seed. +// - We don't do 'current += 2; continue;' after handling ### to keep the code smaller/faster (measured ~10% diff in Debug build) +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashStr(const char* data_p, size_t data_size, ImGuiID seed) { - Filters.resize(0); - TextRange input_range(InputBuf, InputBuf + strlen(InputBuf)); - input_range.split(',', Filters); - - CountGrep = 0; - for (int i = 0; i != Filters.Size; i++) - { - Filters[i].trim_blanks(); - if (Filters[i].empty()) - continue; - if (Filters[i].front() != '-') - CountGrep += 1; - } -} - -bool ImGuiTextFilter::PassFilter(const char *text, const char *text_end) const -{ - if (Filters.empty()) - return true; - - if (text == NULL) - text = ""; - - for (int i = 0; i != Filters.Size; i++) - { - const TextRange &f = Filters[i]; - if (f.empty()) - continue; - if (f.front() == '-') - { - // Subtract - if (ImStristr(text, text_end, f.begin() + 1, f.end()) != NULL) - return false; - } - else - { - // Grep - if (ImStristr(text, text_end, f.begin(), f.end()) != NULL) - return true; - } - } - - // Implicit * grep - if (CountGrep == 0) - return true; - - return false; + seed = ~seed; + ImU32 crc = seed; + const unsigned char* data = (const unsigned char*)data_p; + const ImU32* crc32_lut = GCrc32LookupTable; + if (data_size != 0) + { + while (data_size-- != 0) + { + unsigned char c = *data++; + if (c == '#' && data_size >= 2 && data[0] == '#' && data[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + else + { + while (unsigned char c = *data++) + { + if (c == '#' && data[0] == '#' && data[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + return ~crc; } //----------------------------------------------------------------------------- -// ImGuiTextBuffer +// [SECTION] MISC HELPERS/UTILITIES (File functions) //----------------------------------------------------------------------------- -// On some platform vsnprintf() takes va_list by reference and modifies it. -// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. -#ifndef va_copy -# define va_copy(dest, src) (dest = src) +// Default file functions +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +ImFileHandle ImFileOpen(const char* filename, const char* mode) +{ +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(__CYGWIN__) && !defined(__GNUC__) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. + // Previously we used ImTextCountCharsFromUtf8/ImTextStrFromUtf8 here but we now need to support ImWchar16 and ImWchar32! + const int filename_wsize = ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + const int mode_wsize = ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); + + // Use stack buffer if possible, otherwise heap buffer. Sizes include zero terminator. + // We don't rely on current ImGuiContext as this is implied to be a helper function which doesn't depend on it (see #7314). + wchar_t local_temp_stack[FILENAME_MAX]; + ImVector local_temp_heap; + if (filename_wsize + mode_wsize > IM_ARRAYSIZE(local_temp_stack)) + local_temp_heap.resize(filename_wsize + mode_wsize); + wchar_t* filename_wbuf = local_temp_heap.Data ? local_temp_heap.Data : local_temp_stack; + wchar_t* mode_wbuf = filename_wbuf + filename_wsize; + ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, filename_wbuf, filename_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, mode_wbuf, mode_wsize); + return ::_wfopen(filename_wbuf, mode_wbuf); +#else + return fopen(filename, mode); #endif +} -// Helper: Text buffer for logging/accumulating text -void ImGuiTextBuffer::appendfv(const char *fmt, va_list args) +// We should in theory be using fseeko()/ftello() with off_t and _fseeki64()/_ftelli64() with __int64, waiting for the PR that does that in a very portable pre-C++11 zero-warnings way. +bool ImFileClose(ImFileHandle f) { return fclose(f) == 0; } +ImU64 ImFileGetSize(ImFileHandle f) { long off = 0, sz = 0; return ((off = ftell(f)) != -1 && !fseek(f, 0, SEEK_END) && (sz = ftell(f)) != -1 && !fseek(f, off, SEEK_SET)) ? (ImU64)sz : (ImU64)-1; } +ImU64 ImFileRead(void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fread(data, (size_t)sz, (size_t)count, f); } +ImU64 ImFileWrite(const void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fwrite(data, (size_t)sz, (size_t)count, f); } +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +// Helper: Load file content into memory +// Memory allocated with IM_ALLOC(), must be freed by user using IM_FREE() == ImGui::MemFree() +// This can't really be used with "rt" because fseek size won't match read size. +void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size, int padding_bytes) { - va_list args_copy; - va_copy(args_copy, args); + IM_ASSERT(filename && mode); + if (out_file_size) + *out_file_size = 0; - int len = ImFormatStringV(NULL, 0, fmt, args); // FIXME-OPT: could do a first pass write attempt, likely successful on first pass. - if (len <= 0) - return; + ImFileHandle f; + if ((f = ImFileOpen(filename, mode)) == NULL) + return NULL; - const int write_off = Buf.Size; - const int needed_sz = write_off + len; - if (write_off + len >= Buf.Capacity) - { - int double_capacity = Buf.Capacity * 2; - Buf.reserve(needed_sz > double_capacity ? needed_sz : double_capacity); - } + size_t file_size = (size_t)ImFileGetSize(f); + if (file_size == (size_t)-1) + { + ImFileClose(f); + return NULL; + } - Buf.resize(needed_sz); - ImFormatStringV(&Buf[write_off - 1], len + 1, fmt, args_copy); -} + void* file_data = IM_ALLOC(file_size + padding_bytes); + if (file_data == NULL) + { + ImFileClose(f); + return NULL; + } + if (ImFileRead(file_data, 1, file_size, f) != file_size) + { + ImFileClose(f); + IM_FREE(file_data); + return NULL; + } + if (padding_bytes > 0) + memset((void*)(((char*)file_data) + file_size), 0, (size_t)padding_bytes); -void ImGuiTextBuffer::appendf(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - appendfv(fmt, args); - va_end(args); + ImFileClose(f); + if (out_file_size) + *out_file_size = file_size; + + return file_data; } //----------------------------------------------------------------------------- -// ImGuiSimpleColumns (internal use only) +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) //----------------------------------------------------------------------------- -ImGuiMenuColumns::ImGuiMenuColumns() +IM_MSVC_RUNTIME_CHECKS_OFF + +// Convert UTF-8 to 32-bit character, process single character input. +// A nearly-branchless UTF-8 decoder, based on work of Christopher Wellons (https://github.com/skeeto/branchless-utf8). +// We handle UTF-8 decoding error by skipping forward. +int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end) +{ + static const char lengths[32] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + static const int masks[] = { 0x00, 0x7f, 0x1f, 0x0f, 0x07 }; + static const uint32_t mins[] = { 0x400000, 0, 0x80, 0x800, 0x10000 }; + static const int shiftc[] = { 0, 18, 12, 6, 0 }; + static const int shifte[] = { 0, 6, 4, 2, 0 }; + int len = lengths[*(const unsigned char*)in_text >> 3]; + int wanted = len + (len ? 0 : 1); + + if (in_text_end == NULL) + in_text_end = in_text + wanted; // Max length, nulls will be taken into account. + + // Copy at most 'len' bytes, stop copying at 0 or past in_text_end. Branch predictor does a good job here, + // so it is fast even with excessive branching. + unsigned char s[4]; + s[0] = in_text + 0 < in_text_end ? in_text[0] : 0; + s[1] = in_text + 1 < in_text_end ? in_text[1] : 0; + s[2] = in_text + 2 < in_text_end ? in_text[2] : 0; + s[3] = in_text + 3 < in_text_end ? in_text[3] : 0; + + // Assume a four-byte character and load four bytes. Unused bits are shifted out. + *out_char = (uint32_t)(s[0] & masks[len]) << 18; + *out_char |= (uint32_t)(s[1] & 0x3f) << 12; + *out_char |= (uint32_t)(s[2] & 0x3f) << 6; + *out_char |= (uint32_t)(s[3] & 0x3f) << 0; + *out_char >>= shiftc[len]; + + // Accumulate the various error conditions. + int e = 0; + e = (*out_char < mins[len]) << 6; // non-canonical encoding + e |= ((*out_char >> 11) == 0x1b) << 7; // surrogate half? + e |= (*out_char > IM_UNICODE_CODEPOINT_MAX) << 8; // out of range? + e |= (s[1] & 0xc0) >> 2; + e |= (s[2] & 0xc0) >> 4; + e |= (s[3] ) >> 6; + e ^= 0x2a; // top two bits of each tail byte correct? + e >>= shifte[len]; + + if (e) + { + // No bytes are consumed when *in_text == 0 || in_text == in_text_end. + // One byte is consumed in case of invalid first byte of in_text. + // All available bytes (at most `len` bytes) are consumed on incomplete/invalid second to last bytes. + // Invalid or incomplete input may consume less bytes than wanted, therefore every byte has to be inspected in s. + wanted = ImMin(wanted, !!s[0] + !!s[1] + !!s[2] + !!s[3]); + *out_char = IM_UNICODE_CODEPOINT_INVALID; + } + + return wanted; +} + +int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_text_remaining) { - Count = 0; - Spacing = Width = NextWidth = 0.0f; - memset(Pos, 0, sizeof(Pos)); - memset(NextWidths, 0, sizeof(NextWidths)); + ImWchar* buf_out = buf; + ImWchar* buf_end = buf + buf_size; + while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + *buf_out++ = (ImWchar)c; + } + *buf_out = 0; + if (in_text_remaining) + *in_text_remaining = in_text; + return (int)(buf_out - buf); } -void ImGuiMenuColumns::Update(int count, float spacing, bool clear) +int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end) { - IM_ASSERT(Count <= IM_ARRAYSIZE(Pos)); - Count = count; - Width = NextWidth = 0.0f; - Spacing = spacing; - if (clear) - memset(NextWidths, 0, sizeof(NextWidths)); - for (int i = 0; i < Count; i++) - { - if (i > 0 && NextWidths[i] > 0.0f) - Width += Spacing; - Pos[i] = (float) (int) Width; - Width += NextWidths[i]; - NextWidths[i] = 0.0f; - } + int char_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + char_count++; + } + return char_count; } -float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double +// Based on stb_to_utf8() from github.com/nothings/stb/ +static inline int ImTextCharToUtf8_inline(char* buf, int buf_size, unsigned int c) { - NextWidth = 0.0f; - NextWidths[0] = ImMax(NextWidths[0], w0); - NextWidths[1] = ImMax(NextWidths[1], w1); - NextWidths[2] = ImMax(NextWidths[2], w2); - for (int i = 0; i < 3; i++) - NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f); - return ImMax(Width, NextWidth); + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c ) & 0x3f)); + return 3; + } + if (c <= 0x10FFFF) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c ) & 0x3f)); + return 4; + } + // Invalid code point, the max unicode is 0x10FFFF + return 0; } -float ImGuiMenuColumns::CalcExtraSpace(float avail_w) +const char* ImTextCharToUtf8(char out_buf[5], unsigned int c) { - return ImMax(0.0f, avail_w - Width); + int count = ImTextCharToUtf8_inline(out_buf, 5, c); + out_buf[count] = 0; + return out_buf; } -//----------------------------------------------------------------------------- -// ImGuiListClipper -//----------------------------------------------------------------------------- +// Not optimal but we very rarely use this function. +int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end) +{ + unsigned int unused = 0; + return ImTextCharFromUtf8(&unused, in_text, in_text_end); +} -static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height) -{ - // Set cursor position and a few other things so that SetScrollHere() and Columns() can work when seeking cursor. - // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. Consider moving within SetCursorXXX functions? - ImGui::SetCursorPosY(pos_y); - ImGuiWindow *window = ImGui::GetCurrentWindow(); - window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHere() can properly function after the end of our clipper usage. - window->DC.PrevLineHeight = (line_height - GImGui->Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. - if (window->DC.ColumnsSet) - window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly -} - -// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1 -// Use case B: Begin() called from constructor with items_height>0 -// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style. -void ImGuiListClipper::Begin(int count, float items_height) -{ - StartPosY = ImGui::GetCursorPosY(); - ItemsHeight = items_height; - ItemsCount = count; - StepNo = 0; - DisplayEnd = DisplayStart = -1; - if (ItemsHeight > 0.0f) - { - ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display - if (DisplayStart > 0) - SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor - StepNo = 2; - } +static inline int ImTextCountUtf8BytesFromChar(unsigned int c) +{ + if (c < 0x80) return 1; + if (c < 0x800) return 2; + if (c < 0x10000) return 3; + if (c <= 0x10FFFF) return 4; + return 3; } -void ImGuiListClipper::End() +int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end) { - if (ItemsCount < 0) - return; - // In theory here we should assert that ImGui::GetCursorPosY() == StartPosY + DisplayEnd * ItemsHeight, but it feels saner to just seek at the end and not assert/crash the user. - if (ItemsCount < INT_MAX) - SetCursorPosYAndSetupDummyPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor - ItemsCount = -1; - StepNo = 3; + char* buf_p = out_buf; + const char* buf_end = out_buf + out_buf_size; + while (buf_p < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + *buf_p++ = (char)c; + else + buf_p += ImTextCharToUtf8_inline(buf_p, (int)(buf_end - buf_p - 1), c); + } + *buf_p = 0; + return (int)(buf_p - out_buf); } -bool ImGuiListClipper::Step() +int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) { - if (ItemsCount == 0 || ImGui::GetCurrentWindowRead()->SkipItems) - { - ItemsCount = -1; - return false; - } - if (StepNo == 0) // Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height. - { - DisplayStart = 0; - DisplayEnd = 1; - StartPosY = ImGui::GetCursorPosY(); - StepNo = 1; - return true; - } - if (StepNo == 1) // Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. - { - if (ItemsCount == 1) - { - ItemsCount = -1; - return false; - } - float items_height = ImGui::GetCursorPosY() - StartPosY; - IM_ASSERT(items_height > 0.0f); // If this triggers, it means Item 0 hasn't moved the cursor vertically - Begin(ItemsCount - 1, items_height); - DisplayStart++; - DisplayEnd++; - StepNo = 3; - return true; - } - if (StepNo == 2) // Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user still call Step(). Does nothing and switch to Step 3. - { - IM_ASSERT(DisplayStart >= 0 && DisplayEnd >= 0); - StepNo = 3; - return true; - } - if (StepNo == 3) // Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. - End(); - return false; + int bytes_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + bytes_count++; + else + bytes_count += ImTextCountUtf8BytesFromChar(c); + } + return bytes_count; } -//----------------------------------------------------------------------------- -// ImGuiWindow -//----------------------------------------------------------------------------- +const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr) +{ + while (in_text_curr > in_text_start) + { + in_text_curr--; + if ((*in_text_curr & 0xC0) != 0x80) + return in_text_curr; + } + return in_text_start; +} -ImGuiWindow::ImGuiWindow(ImGuiContext *context, const char *name) -{ - Name = ImStrdup(name); - ID = ImHash(name, 0); - IDStack.push_back(ID); - Flags = 0; - PosFloat = Pos = ImVec2(0.0f, 0.0f); - Size = SizeFull = ImVec2(0.0f, 0.0f); - SizeContents = SizeContentsExplicit = ImVec2(0.0f, 0.0f); - WindowPadding = ImVec2(0.0f, 0.0f); - WindowRounding = 0.0f; - WindowBorderSize = 0.0f; - MoveId = GetID("#MOVE"); - ChildId = 0; - Scroll = ImVec2(0.0f, 0.0f); - ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); - ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); - ScrollbarX = ScrollbarY = false; - ScrollbarSizes = ImVec2(0.0f, 0.0f); - Active = WasActive = false; - WriteAccessed = false; - Collapsed = false; - CollapseToggleWanted = false; - SkipItems = false; - Appearing = false; - CloseButton = false; - BeginOrderWithinParent = -1; - BeginOrderWithinContext = -1; - BeginCount = 0; - PopupId = 0; - AutoFitFramesX = AutoFitFramesY = -1; - AutoFitOnlyGrows = false; - AutoFitChildAxises = 0x00; - AutoPosLastDirection = ImGuiDir_None; - HiddenFrames = 0; - SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; - SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); - - LastFrameActive = -1; - ItemWidthDefault = 0.0f; - FontWindowScale = 1.0f; - - DrawList = IM_NEW(ImDrawList)(&context->DrawListSharedData); - DrawList->_OwnerName = Name; - ParentWindow = NULL; - RootWindow = NULL; - RootWindowForTitleBarHighlight = NULL; - RootWindowForTabbing = NULL; - RootWindowForNav = NULL; - - NavLastIds[0] = NavLastIds[1] = 0; - NavRectRel[0] = NavRectRel[1] = ImRect(); - NavLastChildNavWindow = NULL; - - FocusIdxAllCounter = FocusIdxTabCounter = -1; - FocusIdxAllRequestCurrent = FocusIdxTabRequestCurrent = INT_MAX; - FocusIdxAllRequestNext = FocusIdxTabRequestNext = INT_MAX; +int ImTextCountLines(const char* in_text, const char* in_text_end) +{ + if (in_text_end == NULL) + in_text_end = in_text + strlen(in_text); // FIXME-OPT: Not optimal approach, discourage use for now. + int count = 0; + while (in_text < in_text_end) + { + const char* line_end = (const char*)memchr(in_text, '\n', in_text_end - in_text); + in_text = line_end ? line_end + 1 : in_text_end; + count++; + } + return count; } -ImGuiWindow::~ImGuiWindow() +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// Note: The Convert functions are early design which are not consistent with other API. +//----------------------------------------------------------------------------- + +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b) { - IM_DELETE(DrawList); - IM_DELETE(Name); - for (int i = 0; i != ColumnsStorage.Size; i++) - ColumnsStorage[i].~ImGuiColumnsSet(); + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); } -ImGuiID ImGuiWindow::GetID(const char *str, const char *str_end) +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHash(str, str_end ? (int) (str_end - str) : 0, seed); - ImGui::KeepAliveID(id); - return id; + float s = 1.0f / 255.0f; + return ImVec4( + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); } -ImGuiID ImGuiWindow::GetID(const void *ptr) +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHash(&ptr, sizeof(void *), seed); - ImGui::KeepAliveID(id); - return id; + ImU32 out; + out = ((ImU32)IM_F32_TO_INT8_SAT(in.x)) << IM_COL32_R_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.y)) << IM_COL32_G_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.z)) << IM_COL32_B_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.w)) << IM_COL32_A_SHIFT; + return out; } -ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char *str, const char *str_end) +// Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592 +// Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv +void ImGui::ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v) { - ImGuiID seed = IDStack.back(); - return ImHash(str, str_end ? (int) (str_end - str) : 0, seed); + float K = 0.f; + if (g < b) + { + ImSwap(g, b); + K = -1.f; + } + if (r < g) + { + ImSwap(r, g); + K = -2.f / 6.f - K; + } + + const float chroma = r - (g < b ? g : b); + out_h = ImFabs(K + (g - b) / (6.f * chroma + 1e-20f)); + out_s = chroma / (r + 1e-20f); + out_v = r; } -// This is only used in rare/specific situations to manufacture an ID out of nowhere. -ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect &r_abs) +// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 +// also http://en.wikipedia.org/wiki/HSL_and_HSV +void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) { - ImGuiID seed = IDStack.back(); - const int r_rel[4] = {(int) (r_abs.Min.x - Pos.x), (int) (r_abs.Min.y - Pos.y), (int) (r_abs.Max.x - Pos.x), (int) (r_abs.Max.y - Pos.y)}; - ImGuiID id = ImHash(&r_rel, sizeof(r_rel), seed); - ImGui::KeepAliveID(id); - return id; + if (s == 0.0f) + { + // gray + out_r = out_g = out_b = v; + return; + } + + h = ImFmod(h, 1.0f) / (60.0f / 360.0f); + int i = (int)h; + float f = h - (float)i; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + + switch (i) + { + case 0: out_r = v; out_g = t; out_b = p; break; + case 1: out_r = q; out_g = v; out_b = p; break; + case 2: out_r = p; out_g = v; out_b = t; break; + case 3: out_r = p; out_g = q; out_b = v; break; + case 4: out_r = t; out_g = p; out_b = v; break; + case 5: default: out_r = v; out_g = p; out_b = q; break; + } } //----------------------------------------------------------------------------- -// Internal API exposed in imgui_internal.h +// [SECTION] ImGuiStorage +// Helper: Key->value storage //----------------------------------------------------------------------------- -static void SetCurrentWindow(ImGuiWindow *window) +// std::lower_bound but without the bullshit +ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key) { - ImGuiContext &g = *GImGui; - g.CurrentWindow = window; - if (window) - g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + ImGuiStoragePair* in_p = in_begin; + for (size_t count = (size_t)(in_end - in_p); count > 0; ) + { + size_t count2 = count >> 1; + ImGuiStoragePair* mid = in_p + count2; + if (mid->key < key) + { + in_p = ++mid; + count -= count2 + 1; + } + else + { + count = count2; + } + } + return in_p; } -static void SetNavID(ImGuiID id, int nav_layer) +IM_MSVC_RUNTIME_CHECKS_OFF +static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) { - ImGuiContext &g = *GImGui; - IM_ASSERT(g.NavWindow); - IM_ASSERT(nav_layer == 0 || nav_layer == 1); - g.NavId = id; - g.NavWindow->NavLastIds[nav_layer] = id; + // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. + ImGuiID lhs_v = ((const ImGuiStoragePair*)lhs)->key; + ImGuiID rhs_v = ((const ImGuiStoragePair*)rhs)->key; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); } -static void SetNavIDAndMoveMouse(ImGuiID id, int nav_layer, const ImRect &rect_rel) +// For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. +void ImGuiStorage::BuildSortByKey() { - ImGuiContext &g = *GImGui; - SetNavID(id, nav_layer); - g.NavWindow->NavRectRel[nav_layer] = rect_rel; - g.NavMousePosDirty = true; - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; + ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), PairComparerByID); } -void ImGui::SetActiveID(ImGuiID id, ImGuiWindow *window) +int ImGuiStorage::GetInt(ImGuiID key, int default_val) const { - ImGuiContext &g = *GImGui; - g.ActiveIdIsJustActivated = (g.ActiveId != id); - if (g.ActiveIdIsJustActivated) - g.ActiveIdTimer = 0.0f; - g.ActiveId = id; - g.ActiveIdAllowNavDirFlags = 0; - g.ActiveIdAllowOverlap = false; - g.ActiveIdWindow = window; - if (id) - { - g.ActiveIdIsAlive = true; - g.ActiveIdSource = (g.NavActivateId == id || g.NavInputId == id || g.NavJustTabbedId == id || g.NavJustMovedToId == id) ? ImGuiInputSource_Nav : ImGuiInputSource_Mouse; - } + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return default_val; + return it->val_i; } -ImGuiID ImGui::GetActiveID() +bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const { - ImGuiContext &g = *GImGui; - return g.ActiveId; + return GetInt(key, default_val ? 1 : 0) != 0; } -void ImGui::SetFocusID(ImGuiID id, ImGuiWindow *window) +float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const { - ImGuiContext &g = *GImGui; - IM_ASSERT(id != 0); - - // Assume that SetFocusID() is called in the context where its NavLayer is the current layer, which is the case everywhere we call it. - const int nav_layer = window->DC.NavLayerCurrent; - if (g.NavWindow != window) - g.NavInitRequest = false; - g.NavId = id; - g.NavWindow = window; - g.NavLayer = nav_layer; - window->NavLastIds[nav_layer] = id; - if (window->DC.LastItemId == id) - window->NavRectRel[nav_layer] = ImRect(window->DC.LastItemRect.Min - window->Pos, window->DC.LastItemRect.Max - window->Pos); + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return default_val; + return it->val_f; +} - if (g.ActiveIdSource == ImGuiInputSource_Nav) - g.NavDisableMouseHover = true; - else - g.NavDisableHighlight = true; +void* ImGuiStorage::GetVoidPtr(ImGuiID key) const +{ + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return NULL; + return it->val_p; } -void ImGui::ClearActiveID() +// References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. +int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) { - SetActiveID(0, NULL); + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_i; } -void ImGui::SetHoveredID(ImGuiID id) +bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) { - ImGuiContext &g = *GImGui; - g.HoveredId = id; - g.HoveredIdAllowOverlap = false; - g.HoveredIdTimer = (id != 0 && g.HoveredIdPreviousFrame == id) ? (g.HoveredIdTimer + g.IO.DeltaTime) : 0.0f; + return (bool*)GetIntRef(key, default_val ? 1 : 0); } -ImGuiID ImGui::GetHoveredID() +float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) { - ImGuiContext &g = *GImGui; - return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_f; } -void ImGui::KeepAliveID(ImGuiID id) +void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) { - ImGuiContext &g = *GImGui; - if (g.ActiveId == id) - g.ActiveIdIsAlive = true; + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_p; } -static inline bool IsWindowContentHoverable(ImGuiWindow *window, ImGuiHoveredFlags flags) +// FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) +void ImGuiStorage::SetInt(ImGuiID key, int val) { - // An active popup disable hovering on other windows (apart from its own children) - // FIXME-OPT: This could be cached/stored within the window. - ImGuiContext &g = *GImGui; - if (g.NavWindow) - if (ImGuiWindow *focused_root_window = g.NavWindow->RootWindow) - if (focused_root_window->WasActive && focused_root_window != window->RootWindow) - { - // For the purpose of those flags we differentiate "standard popup" from "modal popup" - // NB: The order of those two tests is important because Modal windows are also Popups. - if (focused_root_window->Flags & ImGuiWindowFlags_Modal) - return false; - if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - return false; - } + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_i = val; +} - return true; +void ImGuiStorage::SetBool(ImGuiID key, bool val) +{ + SetInt(key, val ? 1 : 0); } -// Advance cursor given item size for layout. -void ImGui::ItemSize(const ImVec2 &size, float text_offset_y) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (window->SkipItems) - return; - - // Always align ourselves on pixel boundaries - const float line_height = ImMax(window->DC.CurrentLineHeight, size.y); - const float text_base_offset = ImMax(window->DC.CurrentLineTextBaseOffset, text_offset_y); - // if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] - window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x + size.x, window->DC.CursorPos.y); - window->DC.CursorPos = ImVec2((float) (int) (window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX), (float) (int) (window->DC.CursorPos.y + line_height + g.Style.ItemSpacing.y)); - window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); - window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); - // if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] - - window->DC.PrevLineHeight = line_height; - window->DC.PrevLineTextBaseOffset = text_base_offset; - window->DC.CurrentLineHeight = window->DC.CurrentLineTextBaseOffset = 0.0f; - - // Horizontal layout mode - if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) - SameLine(); -} - -void ImGui::ItemSize(const ImRect &bb, float text_offset_y) -{ - ItemSize(bb.GetSize(), text_offset_y); -} - -static ImGuiDir NavScoreItemGetQuadrant(float dx, float dy) -{ - if (fabsf(dx) > fabsf(dy)) - return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; - return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; -} - -static float NavScoreItemDistInterval(float a0, float a1, float b0, float b1) -{ - if (a1 < b0) - return a1 - b0; - if (b1 < a0) - return a0 - b1; - return 0.0f; -} - -// Scoring function for directional navigation. Based on https://gist.github.com/rygorous/6981057 -static bool NavScoreItem(ImGuiNavMoveResult *result, ImRect cand) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (g.NavLayer != window->DC.NavLayerCurrent) - return false; - - const ImRect &curr = g.NavScoringRectScreen; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) - g.NavScoringCount++; - - // We perform scoring on items bounding box clipped by their parent window on the other axis (clipping on our movement axis would give us equal scores for all clipped items) - if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) - { - cand.Min.y = ImClamp(cand.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y); - cand.Max.y = ImClamp(cand.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y); - } - else - { - cand.Min.x = ImClamp(cand.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); - cand.Max.x = ImClamp(cand.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); - } - - // Compute distance between boxes - // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. - float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); - float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items - if (dby != 0.0f && dbx != 0.0f) - dbx = (dbx / 1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); - float dist_box = fabsf(dbx) + fabsf(dby); - - // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) - float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); - float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); - float dist_center = fabsf(dcx) + fabsf(dcy); // L1 metric (need this for our connectedness guarantee) - - // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance - ImGuiDir quadrant; - float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; - if (dbx != 0.0f || dby != 0.0f) - { - // For non-overlapping boxes, use distance between boxes - dax = dbx; - day = dby; - dist_axial = dist_box; - quadrant = NavScoreItemGetQuadrant(dbx, dby); - } - else if (dcx != 0.0f || dcy != 0.0f) - { - // For overlapping boxes with different centers, use distance between centers - dax = dcx; - day = dcy; - dist_axial = dist_center; - quadrant = NavScoreItemGetQuadrant(dcx, dcy); - } - else - { - // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) - quadrant = (window->DC.LastItemId < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; - } +void ImGuiStorage::SetFloat(ImGuiID key, float val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_f = val; +} -#if IMGUI_DEBUG_NAV_SCORING - char buf[128]; - if (ImGui::IsMouseHoveringRect(cand.Min, cand.Max)) - { - ImFormatString(buf, IM_ARRAYSIZE(buf), "dbox (%.2f,%.2f->%.4f)\ndcen (%.2f,%.2f->%.4f)\nd (%.2f,%.2f->%.4f)\nnav %c, quadrant %c", dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "WENS"[g.NavMoveDir], "WENS"[quadrant]); - g.OverlayDrawList.AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100)); - g.OverlayDrawList.AddRect(cand.Min, cand.Max, IM_COL32(255, 255, 0, 200)); - g.OverlayDrawList.AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + ImGui::CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40, 0, 0, 150)); - g.OverlayDrawList.AddText(g.IO.FontDefault, 13.0f, cand.Max, ~0U, buf); - } - else if (g.IO.KeyCtrl) // Hold to preview score in matching quadrant. Press C to rotate. - { - if (IsKeyPressedMap(ImGuiKey_C)) - { - g.NavMoveDirLast = (ImGuiDir) ((g.NavMoveDirLast + 1) & 3); - g.IO.KeysDownDuration[g.IO.KeyMap[ImGuiKey_C]] = 0.01f; - } - if (quadrant == g.NavMoveDir) - { - ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); - g.OverlayDrawList.AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 200)); - g.OverlayDrawList.AddText(g.IO.FontDefault, 13.0f, cand.Min, IM_COL32(255, 255, 255, 255), buf); - } - } -#endif +void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_p = val; +} - // Is it in the quadrant we're interesting in moving to? - bool new_best = false; - if (quadrant == g.NavMoveDir) - { - // Does it beat the current best candidate? - if (dist_box < result->DistBox) - { - result->DistBox = dist_box; - result->DistCenter = dist_center; - return true; - } - if (dist_box == result->DistBox) - { - // Try using distance between center points to break ties - if (dist_center < result->DistCenter) - { - result->DistCenter = dist_center; - new_best = true; - } - else if (dist_center == result->DistCenter) - { - // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items - // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), - // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. - if (((g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance - new_best = true; - } - } - } - - // Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches - // are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness) - // This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too. - // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. - // Disabling it may however lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? - if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match - if (g.NavLayer == 1 && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) - if ((g.NavMoveDir == ImGuiDir_Left && dax < 0.0f) || (g.NavMoveDir == ImGuiDir_Right && dax > 0.0f) || (g.NavMoveDir == ImGuiDir_Up && day < 0.0f) || (g.NavMoveDir == ImGuiDir_Down && day > 0.0f)) - { - result->DistAxial = dist_axial; - new_best = true; - } - - return new_best; -} - -static void NavSaveLastChildNavWindow(ImGuiWindow *child_window) -{ - ImGuiWindow *parent_window = child_window; - while (parent_window && (parent_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (parent_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) - parent_window = parent_window->ParentWindow; - if (parent_window && parent_window != child_window) - parent_window->NavLastChildNavWindow = child_window; -} - -// Call when we are expected to land on Layer 0 after FocusWindow() -static ImGuiWindow *NavRestoreLastChildNavWindow(ImGuiWindow *window) -{ - return window->NavLastChildNavWindow ? window->NavLastChildNavWindow : window; -} - -static void NavRestoreLayer(int layer) -{ - ImGuiContext &g = *GImGui; - g.NavLayer = layer; - if (layer == 0) - g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); - if (layer == 0 && g.NavWindow->NavLastIds[0] != 0) - SetNavIDAndMoveMouse(g.NavWindow->NavLastIds[0], layer, g.NavWindow->NavRectRel[0]); - else - ImGui::NavInitWindow(g.NavWindow, true); -} - -static inline void NavUpdateAnyRequestFlag() -{ - ImGuiContext &g = *GImGui; - g.NavAnyRequest = g.NavMoveRequest || g.NavInitRequest || IMGUI_DEBUG_NAV_SCORING; -} - -static bool NavMoveRequestButNoResultYet() -{ - ImGuiContext &g = *GImGui; - return g.NavMoveRequest && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +void ImGuiStorage::SetAllInt(int v) +{ + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; } +IM_MSVC_RUNTIME_CHECKS_RESTORE -void ImGui::NavMoveRequestCancel() +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextFilter +//----------------------------------------------------------------------------- + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) //-V1077 { - ImGuiContext &g = *GImGui; - g.NavMoveRequest = false; - NavUpdateAnyRequestFlag(); + InputBuf[0] = 0; + CountGrep = 0; + if (default_filter) + { + ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); + Build(); + } } -// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) -static void ImGui::NavProcessItem(ImGuiWindow *window, const ImRect &nav_bb, const ImGuiID id) -{ - ImGuiContext &g = *GImGui; - // if (!g.IO.NavActive) // [2017/10/06] Removed this possibly redundant test but I am not sure of all the side-effects yet. Some of the feature here will need to work regardless of using a _NoNavInputs flag. - // return; - - const ImGuiItemFlags item_flags = window->DC.ItemFlags; - const ImRect nav_bb_rel(nav_bb.Min - window->Pos, nav_bb.Max - window->Pos); - if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent) - { - // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback - if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus) || g.NavInitResultId == 0) - { - g.NavInitResultId = id; - g.NavInitResultRectRel = nav_bb_rel; - } - if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus)) - { - g.NavInitRequest = false; // Found a match, clear request - NavUpdateAnyRequestFlag(); - } - } - - // Scoring for navigation - if (g.NavId != id && !(item_flags & ImGuiItemFlags_NoNav)) - { - ImGuiNavMoveResult *result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; -#if IMGUI_DEBUG_NAV_SCORING - // [DEBUG] Score all items in NavWindow at all times - if (!g.NavMoveRequest) - g.NavMoveDir = g.NavMoveDirLast; - bool new_best = NavScoreItem(result, nav_bb) && g.NavMoveRequest; -#else - bool new_best = g.NavMoveRequest && NavScoreItem(result, nav_bb); -#endif - if (new_best) - { - result->ID = id; - result->ParentID = window->IDStack.back(); - result->Window = window; - result->RectRel = nav_bb_rel; - } - } - - // Update window-relative bounding box of navigated item - if (g.NavId == id) - { - g.NavWindow = window; // Always refresh g.NavWindow, because some operations such as FocusItem() don't have a window. - g.NavLayer = window->DC.NavLayerCurrent; - g.NavIdIsAlive = true; - g.NavIdTabCounter = window->FocusIdxTabCounter; - window->NavRectRel[window->DC.NavLayerCurrent] = nav_bb_rel; // Store item bounding box (relative to window position) - } +bool ImGuiTextFilter::Draw(const char* label, float width) +{ + if (width != 0.0f) + ImGui::SetNextItemWidth(width); + bool value_changed = ImGui::InputText(label, InputBuf, IM_ARRAYSIZE(InputBuf)); + if (value_changed) + Build(); + return value_changed; } -// Declare item bounding box for clipping and interaction. -// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface -// declare their minimum size requirement to ItemSize() and then use a larger region for drawing/interaction, which is passed to ItemAdd(). -bool ImGui::ItemAdd(const ImRect &bb, ImGuiID id, const ImRect *nav_bb_arg) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - if (id != 0) - { - // Navigation processing runs prior to clipping early-out - // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget - // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests unfortunately, but it is still limited to one window. - // it may not scale very well for windows with ten of thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. - // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick) - window->DC.NavLayerActiveMaskNext |= window->DC.NavLayerCurrentMask; - if (g.NavId == id || g.NavAnyRequest) - if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) - if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) - NavProcessItem(window, nav_bb_arg ? *nav_bb_arg : bb, id); - } - - window->DC.LastItemId = id; - window->DC.LastItemRect = bb; - window->DC.LastItemStatusFlags = 0; - - // Clipping test - const bool is_clipped = IsClippedEx(bb, id, false); - if (is_clipped) - return false; - // if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] - - // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) - if (IsMouseHoveringRect(bb.Min, bb.Max)) - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HoveredRect; - return true; +void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector* out) const +{ + out->resize(0); + const char* wb = b; + const char* we = wb; + while (we < e) + { + if (*we == separator) + { + out->push_back(ImGuiTextRange(wb, we)); + wb = we + 1; + } + we++; + } + if (wb != we) + out->push_back(ImGuiTextRange(wb, we)); } -// This is roughly matching the behavior of internal-facing ItemHoverable() -// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() -// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId -bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +void ImGuiTextFilter::Build() { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (g.NavDisableMouseHover && !g.NavDisableHighlight) - return IsItemFocused(); + Filters.resize(0); + ImGuiTextRange input_range(InputBuf, InputBuf + strlen(InputBuf)); + input_range.split(',', &Filters); - // Test for bounding box overlap, as updated as ItemAdd() - if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) - return false; - IM_ASSERT((flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) == 0); // Flags not supported by this function + CountGrep = 0; + for (ImGuiTextRange& f : Filters) + { + while (f.b < f.e && ImCharIsBlankA(f.b[0])) + f.b++; + while (f.e > f.b && ImCharIsBlankA(f.e[-1])) + f.e--; + if (f.empty()) + continue; + if (f.b[0] != '-') + CountGrep += 1; + } +} - // Test if we are hovering the right window (our window could be behind another window) - // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable to use IsItemHovered() after EndChild() itself. - // Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was the test that has been running for a long while. - // if (g.HoveredWindow != window) - // return false; - if (g.HoveredRootWindow != window->RootWindow && !(flags & ImGuiHoveredFlags_AllowWhenOverlapped)) - return false; +bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const +{ + if (Filters.Size == 0) + return true; - // Test if another item is active (e.g. being dragged) - if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - if (g.ActiveId != 0 && g.ActiveId != window->DC.LastItemId && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) - return false; + if (text == NULL) + text = text_end = ""; - // Test if interactions on this window are blocked by an active popup or modal - if (!IsWindowContentHoverable(window, flags)) - return false; + for (const ImGuiTextRange& f : Filters) + { + if (f.b == f.e) + continue; + if (f.b[0] == '-') + { + // Subtract + if (ImStristr(text, text_end, f.b + 1, f.e) != NULL) + return false; + } + else + { + // Grep + if (ImStristr(text, text_end, f.b, f.e) != NULL) + return true; + } + } - // Test if the item is disabled - if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) - return false; + // Implicit * grep + if (CountGrep == 0) + return true; - // Special handling for the 1st item after Begin() which represent the title bar. When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect tht case. - if (window->DC.LastItemId == window->MoveId && window->WriteAccessed) - return false; - return true; + return false; } -// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). -bool ImGui::ItemHoverable(const ImRect &bb, ImGuiID id) +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex +//----------------------------------------------------------------------------- + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#if defined(__GNUC__) || defined(__clang__) +#define va_copy(dest, src) __builtin_va_copy(dest, src) +#else +#define va_copy(dest, src) (dest = src) +#endif +#endif + +char ImGuiTextBuffer::EmptyString[1] = { 0 }; + +void ImGuiTextBuffer::append(const char* str, const char* str_end) { - ImGuiContext &g = *GImGui; - if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) - return false; + int len = str_end ? (int)(str_end - str) : (int)strlen(str); - ImGuiWindow *window = g.CurrentWindow; - if (g.HoveredWindow != window) - return false; - if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) - return false; - if (!IsMouseHoveringRect(bb.Min, bb.Max)) - return false; - if (g.NavDisableMouseHover || !IsWindowContentHoverable(window, ImGuiHoveredFlags_Default)) - return false; - if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) - return false; + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } - SetHoveredID(id); - return true; + Buf.resize(needed_sz); + memcpy(&Buf[write_off - 1], str, (size_t)len); + Buf[write_off - 1 + len] = 0; } -bool ImGui::IsClippedEx(const ImRect &bb, ImGuiID id, bool clip_even_when_logged) +void ImGuiTextBuffer::appendf(const char* fmt, ...) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (!bb.Overlaps(window->ClipRect)) - if (id == 0 || id != g.ActiveId) - if (clip_even_when_logged || !g.LogEnabled) - return true; - return false; + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); } -bool ImGui::FocusableItemRegister(ImGuiWindow *window, ImGuiID id, bool tab_stop) +// Helper: Text buffer for logging/accumulating text +void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) { - ImGuiContext &g = *GImGui; - - const bool allow_keyboard_focus = (window->DC.ItemFlags & (ImGuiItemFlags_AllowKeyboardFocus | ImGuiItemFlags_Disabled)) == ImGuiItemFlags_AllowKeyboardFocus; - window->FocusIdxAllCounter++; - if (allow_keyboard_focus) - window->FocusIdxTabCounter++; + va_list args_copy; + va_copy(args_copy, args); - // Process keyboard input at this point: TAB/Shift-TAB to tab out of the currently focused item. - // Note that we can always TAB out of a widget that doesn't allow tabbing in. - if (tab_stop && (g.ActiveId == id) && window->FocusIdxAllRequestNext == INT_MAX && window->FocusIdxTabRequestNext == INT_MAX && !g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab)) - window->FocusIdxTabRequestNext = window->FocusIdxTabCounter + (g.IO.KeyShift ? (allow_keyboard_focus ? -1 : 0) : +1); // Modulo on index will be applied at the end of frame once we've got the total counter of items. + int len = ImFormatStringV(NULL, 0, fmt, args); // FIXME-OPT: could do a first pass write attempt, likely successful on first pass. + if (len <= 0) + { + va_end(args_copy); + return; + } - if (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent) - return true; - if (allow_keyboard_focus && window->FocusIdxTabCounter == window->FocusIdxTabRequestCurrent) - { - g.NavJustTabbedId = id; - return true; - } + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } - return false; + Buf.resize(needed_sz); + ImFormatStringV(&Buf[write_off - 1], (size_t)len + 1, fmt, args_copy); + va_end(args_copy); } -void ImGui::FocusableItemUnregister(ImGuiWindow *window) +void ImGuiTextIndex::append(const char* base, int old_size, int new_size) { - window->FocusIdxAllCounter--; - window->FocusIdxTabCounter--; + IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset); + if (old_size == new_size) + return; + if (EndOffset == 0 || base[EndOffset - 1] == '\n') + LineOffsets.push_back(EndOffset); + const char* base_end = base + new_size; + for (const char* p = base + old_size; (p = (const char*)memchr(p, '\n', base_end - p)) != 0; ) + if (++p < base_end) // Don't push a trailing offset on last \n + LineOffsets.push_back((int)(intptr_t)(p - base)); + EndOffset = ImMax(EndOffset, new_size); } -ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_x, float default_y) +//----------------------------------------------------------------------------- +// [SECTION] ImGuiListClipper +//----------------------------------------------------------------------------- + +// FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. +// The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous. +static bool GetSkipItemForListClipping() { - ImGuiContext &g = *GImGui; - ImVec2 content_max; - if (size.x < 0.0f || size.y < 0.0f) - content_max = g.CurrentWindow->Pos + GetContentRegionMax(); - if (size.x <= 0.0f) - size.x = (size.x == 0.0f) ? default_x : ImMax(content_max.x - g.CurrentWindow->DC.CursorPos.x, 4.0f) + size.x; - if (size.y <= 0.0f) - size.y = (size.y == 0.0f) ? default_y : ImMax(content_max.y - g.CurrentWindow->DC.CursorPos.y, 4.0f) + size.y; - return size; + ImGuiContext& g = *GImGui; + return (g.CurrentTable ? g.CurrentTable->HostSkipItems : g.CurrentWindow->SkipItems); } -float ImGui::CalcWrapWidthForPos(const ImVec2 &pos, float wrap_pos_x) +static void ImGuiListClipper_SortAndFuseRanges(ImVector& ranges, int offset = 0) { - if (wrap_pos_x < 0.0f) - return 0.0f; + if (ranges.Size - offset <= 1) + return; - ImGuiWindow *window = GetCurrentWindowRead(); - if (wrap_pos_x == 0.0f) - wrap_pos_x = GetContentRegionMax().x + window->Pos.x; - else if (wrap_pos_x > 0.0f) - wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + // Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries) + for (int sort_end = ranges.Size - offset - 1; sort_end > 0; --sort_end) + for (int i = offset; i < sort_end + offset; ++i) + if (ranges[i].Min > ranges[i + 1].Min) + ImSwap(ranges[i], ranges[i + 1]); - return ImMax(wrap_pos_x - pos.x, 1.0f); + // Now fuse ranges together as much as possible. + for (int i = 1 + offset; i < ranges.Size; i++) + { + IM_ASSERT(!ranges[i].PosToIndexConvert && !ranges[i - 1].PosToIndexConvert); + if (ranges[i - 1].Max < ranges[i].Min) + continue; + ranges[i - 1].Min = ImMin(ranges[i - 1].Min, ranges[i].Min); + ranges[i - 1].Max = ImMax(ranges[i - 1].Max, ranges[i].Max); + ranges.erase(ranges.Data + i); + i--; + } } -//----------------------------------------------------------------------------- - -void *ImGui::MemAlloc(size_t sz) -{ - GImAllocatorActiveAllocationsCount++; - return GImAllocatorAllocFunc(sz, GImAllocatorUserData); +static void ImGuiListClipper_SeekCursorAndSetupPrevLine(float pos_y, float line_height) +{ + // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor. + // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. + // The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek? + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float off_y = pos_y - window->DC.CursorPos.y; + window->DC.CursorPos.y = pos_y; + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y - g.Style.ItemSpacing.y); + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage. + window->DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. + if (ImGuiOldColumns* columns = window->DC.CurrentColumns) + columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly + if (ImGuiTable* table = g.CurrentTable) + { + if (table->IsInsideRow) + ImGui::TableEndRow(table); + table->RowPosY2 = window->DC.CursorPos.y; + const int row_increase = (int)((off_y / line_height) + 0.5f); + //table->CurrentRow += row_increase; // Can't do without fixing TableEndRow() + table->RowBgColorCounter += row_increase; + } } -void ImGui::MemFree(void *ptr) +ImGuiListClipper::ImGuiListClipper() { - if (ptr) - GImAllocatorActiveAllocationsCount--; - return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); + memset(this, 0, sizeof(*this)); } -const char *ImGui::GetClipboardText() +ImGuiListClipper::~ImGuiListClipper() { - return GImGui->IO.GetClipboardTextFn ? GImGui->IO.GetClipboardTextFn(GImGui->IO.ClipboardUserData) : ""; + End(); } -void ImGui::SetClipboardText(const char *text) +void ImGuiListClipper::Begin(int items_count, float items_height) { - if (GImGui->IO.SetClipboardTextFn) - GImGui->IO.SetClipboardTextFn(GImGui->IO.ClipboardUserData, text); -} + if (Ctx == NULL) + Ctx = ImGui::GetCurrentContext(); -const char *ImGui::GetVersion() -{ - return IMGUI_VERSION; -} + ImGuiContext& g = *Ctx; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Begin(%d,%.2f) in '%s'\n", items_count, items_height, window->Name); -// Internal state access - if you want to share ImGui state between modules (e.g. DLL) or allocate it yourself -// Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module -ImGuiContext *ImGui::GetCurrentContext() -{ - return GImGui; + if (ImGuiTable* table = g.CurrentTable) + if (table->IsInsideRow) + ImGui::TableEndRow(table); + + StartPosY = window->DC.CursorPos.y; + ItemsHeight = items_height; + ItemsCount = items_count; + DisplayStart = -1; + DisplayEnd = 0; + + // Acquire temporary buffer + if (++g.ClipperTempDataStacked > g.ClipperTempData.Size) + g.ClipperTempData.resize(g.ClipperTempDataStacked, ImGuiListClipperData()); + ImGuiListClipperData* data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->Reset(this); + data->LossynessOffset = window->DC.CursorStartPosLossyness.y; + TempData = data; + StartSeekOffsetY = data->LossynessOffset; } -void ImGui::SetCurrentContext(ImGuiContext *ctx) +void ImGuiListClipper::End() { -#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC - IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. -#else - GImGui = ctx; -#endif + if (ImGuiListClipperData* data = (ImGuiListClipperData*)TempData) + { + // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user. + ImGuiContext& g = *Ctx; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: End() in '%s'\n", g.CurrentWindow->Name); + if (ItemsCount >= 0 && ItemsCount < INT_MAX && DisplayStart >= 0) + SeekCursorForItem(ItemsCount); + + // Restore temporary buffer and fix back pointers which may be invalidated when nesting + IM_ASSERT(data->ListClipper == this); + data->StepNo = data->Ranges.Size; + if (--g.ClipperTempDataStacked > 0) + { + data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->ListClipper->TempData = data; + } + TempData = NULL; + } + ItemsCount = -1; } -void ImGui::SetAllocatorFunctions(void *(*alloc_func)(size_t sz, void *user_data), void (*free_func)(void *ptr, void *user_data), void *user_data) +void ImGuiListClipper::IncludeItemsByIndex(int item_begin, int item_end) { - GImAllocatorAllocFunc = alloc_func; - GImAllocatorFreeFunc = free_func; - GImAllocatorUserData = user_data; + ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; + IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet. + IM_ASSERT(item_begin <= item_end); + if (item_begin < item_end) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(item_begin, item_end)); } -ImGuiContext *ImGui::CreateContext(ImFontAtlas *shared_font_atlas) +// This is already called while stepping. +// The ONLY reason you may want to call this is if you passed INT_MAX to ImGuiListClipper::Begin() because you couldn't step item count beforehand. +void ImGuiListClipper::SeekCursorForItem(int item_n) { - ImGuiContext *ctx = IM_NEW(ImGuiContext)(shared_font_atlas); - if (GImGui == NULL) - SetCurrentContext(ctx); - Initialize(ctx); - return ctx; + // - Perform the add and multiply with double to allow seeking through larger ranges. + // - StartPosY starts from ItemsFrozen, by adding SeekOffsetY we generally cancel that out (SeekOffsetY == LossynessOffset - ItemsFrozen * ItemsHeight). + // - The reason we store SeekOffsetY instead of inferring it, is because we want to allow user to perform Seek after the last step, where ImGuiListClipperData is already done. + float pos_y = (float)((double)StartPosY + StartSeekOffsetY + (double)item_n * ItemsHeight); + ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, ItemsHeight); } -void ImGui::DestroyContext(ImGuiContext *ctx) +static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) { - if (ctx == NULL) - ctx = GImGui; - Shutdown(ctx); - if (GImGui == ctx) - SetCurrentContext(NULL); - IM_DELETE(ctx); + ImGuiContext& g = *clipper->Ctx; + ImGuiWindow* window = g.CurrentWindow; + ImGuiListClipperData* data = (ImGuiListClipperData*)clipper->TempData; + IM_ASSERT(data != NULL && "Called ImGuiListClipper::Step() too many times, or before ImGuiListClipper::Begin() ?"); + + ImGuiTable* table = g.CurrentTable; + if (table && table->IsInsideRow) + ImGui::TableEndRow(table); + + // No items + if (clipper->ItemsCount == 0 || GetSkipItemForListClipping()) + return false; + + // While we are in frozen row state, keep displaying items one by one, unclipped + // FIXME: Could be stored as a table-agnostic state. + if (data->StepNo == 0 && table != NULL && !table->IsUnfrozenRows) + { + clipper->DisplayStart = data->ItemsFrozen; + clipper->DisplayEnd = ImMin(data->ItemsFrozen + 1, clipper->ItemsCount); + if (clipper->DisplayStart < clipper->DisplayEnd) + data->ItemsFrozen++; + return true; + } + + // Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height) + bool calc_clipping = false; + if (data->StepNo == 0) + { + clipper->StartPosY = window->DC.CursorPos.y; + if (clipper->ItemsHeight <= 0.0f) + { + // Submit the first item (or range) so we can measure its height (generally the first range is 0..1) + data->Ranges.push_front(ImGuiListClipperRange::FromIndices(data->ItemsFrozen, data->ItemsFrozen + 1)); + clipper->DisplayStart = ImMax(data->Ranges[0].Min, data->ItemsFrozen); + clipper->DisplayEnd = ImMin(data->Ranges[0].Max, clipper->ItemsCount); + data->StepNo = 1; + return true; + } + calc_clipping = true; // If on the first step with known item height, calculate clipping. + } + + // Step 1: Let the clipper infer height from first range + if (clipper->ItemsHeight <= 0.0f) + { + IM_ASSERT(data->StepNo == 1); + if (table) + IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y); + + clipper->ItemsHeight = (window->DC.CursorPos.y - clipper->StartPosY) / (float)(clipper->DisplayEnd - clipper->DisplayStart); + bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision(clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y); + if (affected_by_floating_point_precision) + clipper->ItemsHeight = window->DC.PrevLineSize.y + g.Style.ItemSpacing.y; // FIXME: Technically wouldn't allow multi-line entries. + if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode. + return false; + IM_ASSERT(clipper->ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); + calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. + } + + // Step 0 or 1: Calculate the actual ranges of visible elements. + const int already_submitted = clipper->DisplayEnd; + if (calc_clipping) + { + // Record seek offset, this is so ImGuiListClipper::Seek() can be called after ImGuiListClipperData is done + clipper->StartSeekOffsetY = (double)data->LossynessOffset - data->ItemsFrozen * (double)clipper->ItemsHeight; + + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(0, clipper->ItemsCount)); + } + else + { + // Add range selected to be included for navigation + const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (is_nav_request) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0)); + if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && g.NavTabbingDir == -1) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(clipper->ItemsCount - 1, clipper->ItemsCount)); + + // Add focused/active item + ImRect nav_rect_abs = ImGui::WindowRectRelToAbs(window, window->NavRectRel[0]); + if (g.NavId != 0 && window->NavLastIds[0] == g.NavId) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); + + // Add visible range + float min_y = window->ClipRect.Min.y; + float max_y = window->ClipRect.Max.y; + + // Add box selection range + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (bs->IsActive && bs->Window == window) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (bs->UnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); + } + + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; + const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max)); + } + + // Convert position ranges to item index ranges + // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping. + // - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list, + // which with the flooring/ceiling tend to lead to 2 items instead of one being submitted. + for (ImGuiListClipperRange& range : data->Ranges) + if (range.PosToIndexConvert) + { + int m1 = (int)(((double)range.Min - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight); + int m2 = (int)((((double)range.Max - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight) + 0.999999f); + range.Min = ImClamp(already_submitted + m1 + range.PosToIndexOffsetMin, already_submitted, clipper->ItemsCount - 1); + range.Max = ImClamp(already_submitted + m2 + range.PosToIndexOffsetMax, range.Min + 1, clipper->ItemsCount); + range.PosToIndexConvert = false; + } + ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo); + } + + // Step 0+ (if item height is given in advance) or 1+: Display the next range in line. + while (data->StepNo < data->Ranges.Size) + { + clipper->DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted); + clipper->DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, clipper->ItemsCount); + if (clipper->DisplayStart > already_submitted) //-V1051 + clipper->SeekCursorForItem(clipper->DisplayStart); + data->StepNo++; + if (clipper->DisplayStart == clipper->DisplayEnd && data->StepNo < data->Ranges.Size) + continue; + return true; + } + + // After the last step: Let the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), + // Advance the cursor to the end of the list and then returns 'false' to end the loop. + if (clipper->ItemsCount < INT_MAX) + clipper->SeekCursorForItem(clipper->ItemsCount); + + return false; } -ImGuiIO &ImGui::GetIO() +bool ImGuiListClipper::Step() { - IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); - return GImGui->IO; + ImGuiContext& g = *Ctx; + bool need_items_height = (ItemsHeight <= 0.0f); + bool ret = ImGuiListClipper_StepInternal(this); + if (ret && (DisplayStart == DisplayEnd)) + ret = false; + if (g.CurrentTable && g.CurrentTable->IsUnfrozenRows == false) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): inside frozen table row.\n"); + if (need_items_height && ItemsHeight > 0.0f) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): computed ItemsHeight: %.2f.\n", ItemsHeight); + if (ret) + { + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): display %d to %d.\n", DisplayStart, DisplayEnd); + } + else + { + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): End.\n"); + End(); + } + return ret; } -ImGuiStyle &ImGui::GetStyle() +//----------------------------------------------------------------------------- +// [SECTION] STYLING +//----------------------------------------------------------------------------- + +ImGuiStyle& ImGui::GetStyle() { - IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); - return GImGui->Style; + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->Style; } -// Same value as passed to the old io.RenderDrawListsFn function. Valid after Render() and until the next call to NewFrame() -ImDrawData *ImGui::GetDrawData() +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) { - ImGuiContext &g = *GImGui; - return g.DrawData.Valid ? &g.DrawData : NULL; + ImGuiStyle& style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); } -float ImGui::GetTime() +ImU32 ImGui::GetColorU32(const ImVec4& col) { - return GImGui->Time; + ImGuiStyle& style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); } -int ImGui::GetFrameCount() +const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) { - return GImGui->FrameCount; + ImGuiStyle& style = GImGui->Style; + return style.Colors[idx]; } -ImDrawList *ImGui::GetOverlayDrawList() +ImU32 ImGui::GetColorU32(ImU32 col, float alpha_mul) { - return &GImGui->OverlayDrawList; + ImGuiStyle& style = GImGui->Style; + alpha_mul *= style.Alpha; + if (alpha_mul >= 1.0f) + return col; + ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; + a = (ImU32)(a * alpha_mul); // We don't need to clamp 0..255 because alpha is in 0..1 range. + return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); } -ImDrawListSharedData *ImGui::GetDrawListSharedData() +// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 +void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) { - return &GImGui->DrawListSharedData; + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); } -// This needs to be called before we submit any widget (aka in or before Begin) -void ImGui::NavInitWindow(ImGuiWindow *window, bool force_reinit) +void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) { - ImGuiContext &g = *GImGui; - IM_ASSERT(window == g.NavWindow); - bool init_for_nav = false; - if (!(window->Flags & ImGuiWindowFlags_NoNavInputs)) - if (!(window->Flags & ImGuiWindowFlags_ChildWindow) || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) - init_for_nav = true; - if (init_for_nav) - { - SetNavID(0, g.NavLayer); - g.NavInitRequest = true; - g.NavInitRequestFromMove = false; - g.NavInitResultId = 0; - g.NavInitResultRectRel = ImRect(); - NavUpdateAnyRequestFlag(); - } - else - { - g.NavId = window->NavLastIds[0]; - } + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = col; } -static ImVec2 NavCalcPreferredMousePos() +void ImGui::PopStyleColor(int count) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.NavWindow; - if (!window) - return g.IO.MousePos; - const ImRect &rect_rel = window->NavRectRel[g.NavLayer]; - ImVec2 pos = g.NavWindow->Pos + ImVec2(rect_rel.Min.x + ImMin(g.Style.FramePadding.x * 4, rect_rel.GetWidth()), rect_rel.Max.y - ImMin(g.Style.FramePadding.y, rect_rel.GetHeight())); - ImRect visible_rect = GetViewportRect(); - return ImFloor(ImClamp(pos, visible_rect.Min, visible_rect.Max)); // ImFloor() is important because non-integer mouse position application in back-end might be lossy and result in undesirable non-zero delta. + ImGuiContext& g = *GImGui; + if (g.ColorStack.Size < count) + { + IM_ASSERT_USER_ERROR(g.ColorStack.Size > count, "Calling PopStyleColor() too many times!"); + count = g.ColorStack.Size; + } + while (count > 0) + { + ImGuiColorMod& backup = g.ColorStack.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorStack.pop_back(); + count--; + } } -static int FindWindowIndex(ImGuiWindow *window) // FIXME-OPT O(N) +static const ImGuiDataVarInfo GStyleVarInfo[] = +{ + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBorderSize) }, // ImGuiStyleVar_TabBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TabBarOverlineSize) }, // ImGuiStyleVar_TabBarOverlineSize + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign + { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding +}; + +const ImGuiDataVarInfo* ImGui::GetStyleVarInfo(ImGuiStyleVar idx) { - ImGuiContext &g = *GImGui; - for (int i = g.Windows.Size - 1; i >= 0; i--) - if (g.Windows[i] == window) - return i; - return -1; + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_COUNT); + IM_STATIC_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_COUNT); + return &GStyleVarInfo[idx]; } -static ImGuiWindow *FindWindowNavigable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) { - ImGuiContext &g = *GImGui; - for (int i = i_start; i >= 0 && i < g.Windows.Size && i != i_stop; i += dir) - if (ImGui::IsWindowNavFocusable(g.Windows[i])) - return g.Windows[i]; - return NULL; + ImGuiContext& g = *GImGui; + const ImGuiDataVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type != ImGuiDataType_Float || var_info->Count != 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + float* pvar = (float*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; } -float ImGui::GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode) +void ImGui::PushStyleVarX(ImGuiStyleVar idx, float val_x) { - ImGuiContext &g = *GImGui; - if (mode == ImGuiInputReadMode_Down) - return g.IO.NavInputs[n]; // Instant, read analog input (0.0f..1.0f, as provided by user) - - const float t = g.IO.NavInputsDownDuration[n]; - if (t < 0.0f && mode == ImGuiInputReadMode_Released) // Return 1.0f when just released, no repeat, ignore analog input. - return (g.IO.NavInputsDownDurationPrev[n] >= 0.0f ? 1.0f : 0.0f); - if (t < 0.0f) - return 0.0f; - if (mode == ImGuiInputReadMode_Pressed) // Return 1.0f when just pressed, no repeat, ignore analog input. - return (t == 0.0f) ? 1.0f : 0.0f; - if (mode == ImGuiInputReadMode_Repeat) - return (float) CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.80f); - if (mode == ImGuiInputReadMode_RepeatSlow) - return (float) CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 1.00f, g.IO.KeyRepeatRate * 2.00f); - if (mode == ImGuiInputReadMode_RepeatFast) - return (float) CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.30f); - return 0.0f; + ImGuiContext& g = *GImGui; + const ImGuiDataVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->x = val_x; } -// Equivalent of IsKeyDown() for NavInputs[] -static bool IsNavInputDown(ImGuiNavInput n) +void ImGui::PushStyleVarY(ImGuiStyleVar idx, float val_y) { - return GImGui->IO.NavInputs[n] > 0.0f; + ImGuiContext& g = *GImGui; + const ImGuiDataVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->y = val_y; } -// Equivalent of IsKeyPressed() for NavInputs[] -static bool IsNavInputPressed(ImGuiNavInput n, ImGuiInputReadMode mode) +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) { - return ImGui::GetNavInputAmount(n, mode) > 0.0f; + ImGuiContext& g = *GImGui; + const ImGuiDataVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->Type != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; } -static bool IsNavInputPressedAnyOfTwo(ImGuiNavInput n1, ImGuiNavInput n2, ImGuiInputReadMode mode) +void ImGui::PopStyleVar(int count) { - return (ImGui::GetNavInputAmount(n1, mode) + ImGui::GetNavInputAmount(n2, mode)) > 0.0f; + ImGuiContext& g = *GImGui; + if (g.StyleVarStack.Size < count) + { + IM_ASSERT_USER_ERROR(g.StyleVarStack.Size > count, "Calling PopStyleVar() too many times!"); + count = g.StyleVarStack.Size; + } + while (count > 0) + { + // We avoid a generic memcpy(data, &backup.Backup.., GDataTypeSize[info->Type] * info->Count), the overhead in Debug is not worth it. + ImGuiStyleMod& backup = g.StyleVarStack.back(); + const ImGuiDataVarInfo* info = GetStyleVarInfo(backup.VarIdx); + void* data = info->GetVarPtr(&g.Style); + if (info->Type == ImGuiDataType_Float && info->Count == 1) { ((float*)data)[0] = backup.BackupFloat[0]; } + else if (info->Type == ImGuiDataType_Float && info->Count == 2) { ((float*)data)[0] = backup.BackupFloat[0]; ((float*)data)[1] = backup.BackupFloat[1]; } + g.StyleVarStack.pop_back(); + count--; + } } -ImVec2 ImGui::GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor, float fast_factor) +const char* ImGui::GetStyleColorName(ImGuiCol idx) { - ImVec2 delta(0.0f, 0.0f); - if (dir_sources & ImGuiNavDirSourceFlags_Keyboard) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_KeyRight_, mode) - GetNavInputAmount(ImGuiNavInput_KeyLeft_, mode), GetNavInputAmount(ImGuiNavInput_KeyDown_, mode) - GetNavInputAmount(ImGuiNavInput_KeyUp_, mode)); - if (dir_sources & ImGuiNavDirSourceFlags_PadDPad) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_DpadRight, mode) - GetNavInputAmount(ImGuiNavInput_DpadLeft, mode), GetNavInputAmount(ImGuiNavInput_DpadDown, mode) - GetNavInputAmount(ImGuiNavInput_DpadUp, mode)); - if (dir_sources & ImGuiNavDirSourceFlags_PadLStick) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_LStickRight, mode) - GetNavInputAmount(ImGuiNavInput_LStickLeft, mode), GetNavInputAmount(ImGuiNavInput_LStickDown, mode) - GetNavInputAmount(ImGuiNavInput_LStickUp, mode)); - if (slow_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta *= slow_factor; - if (fast_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakFast)) - delta *= fast_factor; - return delta; + // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; + switch (idx) + { + case ImGuiCol_Text: return "Text"; + case ImGuiCol_TextDisabled: return "TextDisabled"; + case ImGuiCol_WindowBg: return "WindowBg"; + case ImGuiCol_ChildBg: return "ChildBg"; + case ImGuiCol_PopupBg: return "PopupBg"; + case ImGuiCol_Border: return "Border"; + case ImGuiCol_BorderShadow: return "BorderShadow"; + case ImGuiCol_FrameBg: return "FrameBg"; + case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; + case ImGuiCol_FrameBgActive: return "FrameBgActive"; + case ImGuiCol_TitleBg: return "TitleBg"; + case ImGuiCol_TitleBgActive: return "TitleBgActive"; + case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; + case ImGuiCol_MenuBarBg: return "MenuBarBg"; + case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; + case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; + case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; + case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; + case ImGuiCol_CheckMark: return "CheckMark"; + case ImGuiCol_SliderGrab: return "SliderGrab"; + case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; + case ImGuiCol_Button: return "Button"; + case ImGuiCol_ButtonHovered: return "ButtonHovered"; + case ImGuiCol_ButtonActive: return "ButtonActive"; + case ImGuiCol_Header: return "Header"; + case ImGuiCol_HeaderHovered: return "HeaderHovered"; + case ImGuiCol_HeaderActive: return "HeaderActive"; + case ImGuiCol_Separator: return "Separator"; + case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; + case ImGuiCol_SeparatorActive: return "SeparatorActive"; + case ImGuiCol_ResizeGrip: return "ResizeGrip"; + case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; + case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; + case ImGuiCol_TabHovered: return "TabHovered"; + case ImGuiCol_Tab: return "Tab"; + case ImGuiCol_TabSelected: return "TabSelected"; + case ImGuiCol_TabSelectedOverline: return "TabSelectedOverline"; + case ImGuiCol_TabDimmed: return "TabDimmed"; + case ImGuiCol_TabDimmedSelected: return "TabDimmedSelected"; + case ImGuiCol_TabDimmedSelectedOverline: return "TabDimmedSelectedOverline"; + case ImGuiCol_PlotLines: return "PlotLines"; + case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; + case ImGuiCol_PlotHistogram: return "PlotHistogram"; + case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TableHeaderBg: return "TableHeaderBg"; + case ImGuiCol_TableBorderStrong: return "TableBorderStrong"; + case ImGuiCol_TableBorderLight: return "TableBorderLight"; + case ImGuiCol_TableRowBg: return "TableRowBg"; + case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt"; + case ImGuiCol_TextLink: return "TextLink"; + case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; + case ImGuiCol_DragDropTarget: return "DragDropTarget"; + case ImGuiCol_NavHighlight: return "NavHighlight"; + case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; + case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; + case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; + } + IM_ASSERT(0); + return "Unknown"; } -static void NavUpdateWindowingHighlightWindow(int focus_change_dir) -{ - ImGuiContext &g = *GImGui; - IM_ASSERT(g.NavWindowingTarget); - if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) - return; - const int i_current = FindWindowIndex(g.NavWindowingTarget); - ImGuiWindow *window_target = FindWindowNavigable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); - if (!window_target) - window_target = FindWindowNavigable((focus_change_dir < 0) ? (g.Windows.Size - 1) : 0, i_current, focus_change_dir); - g.NavWindowingTarget = window_target; - g.NavWindowingToggleLayer = false; -} +//----------------------------------------------------------------------------- +// [SECTION] RENDER HELPERS +// Some of those (internal) functions are currently quite a legacy mess - their signature and behavior will change, +// we need a nicer separation between low-level functions and high-level functions relying on the ImGui context. +// Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: context. +//----------------------------------------------------------------------------- -// Window management mode (hold to: change focus/move/resize, tap to: toggle menu layer) -static void ImGui::NavUpdateWindowing() +const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) { - ImGuiContext &g = *GImGui; - ImGuiWindow *apply_focus_window = NULL; - bool apply_toggle_layer = false; - - bool start_windowing_with_gamepad = !g.NavWindowingTarget && IsNavInputPressed(ImGuiNavInput_Menu, ImGuiInputReadMode_Pressed); - bool start_windowing_with_keyboard = !g.NavWindowingTarget && g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab) && (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard); - if (start_windowing_with_gamepad || start_windowing_with_keyboard) - if (ImGuiWindow *window = g.NavWindow ? g.NavWindow : FindWindowNavigable(g.Windows.Size - 1, -INT_MAX, -1)) - { - g.NavWindowingTarget = window->RootWindowForTabbing; - g.NavWindowingHighlightTimer = g.NavWindowingHighlightAlpha = 0.0f; - g.NavWindowingToggleLayer = start_windowing_with_keyboard ? false : true; - g.NavWindowingInputSource = start_windowing_with_keyboard ? ImGuiInputSource_NavKeyboard : ImGuiInputSource_NavGamepad; - } - - // Gamepad update - g.NavWindowingHighlightTimer += g.IO.DeltaTime; - if (g.NavWindowingTarget && g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) - { - // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise - g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingHighlightTimer - 0.20f) / 0.05f)); - - // Select window to focus - const int focus_change_dir = (int) IsNavInputPressed(ImGuiNavInput_FocusPrev, ImGuiInputReadMode_RepeatSlow) - (int) IsNavInputPressed(ImGuiNavInput_FocusNext, ImGuiInputReadMode_RepeatSlow); - if (focus_change_dir != 0) - { - NavUpdateWindowingHighlightWindow(focus_change_dir); - g.NavWindowingHighlightAlpha = 1.0f; - } - - // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered front-most) - if (!IsNavInputDown(ImGuiNavInput_Menu)) - { - g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. - if (g.NavWindowingToggleLayer && g.NavWindow) - apply_toggle_layer = true; - else if (!g.NavWindowingToggleLayer) - apply_focus_window = g.NavWindowingTarget; - g.NavWindowingTarget = NULL; - } - } - - // Keyboard: Focus - if (g.NavWindowingTarget && g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard) - { - // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise - g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingHighlightTimer - 0.15f) / 0.04f)); // 1.0f - if (IsKeyPressedMap(ImGuiKey_Tab, true)) - NavUpdateWindowingHighlightWindow(g.IO.KeyShift ? +1 : -1); - if (!g.IO.KeyCtrl) - apply_focus_window = g.NavWindowingTarget; - } - - // Keyboard: Press and Release ALT to toggle menu layer - // FIXME: We lack an explicit IO variable for "is the imgui window focused", so compare mouse validity to detect the common case of back-end clearing releases all keys on ALT-TAB - if ((g.ActiveId == 0 || g.ActiveIdAllowOverlap) && IsNavInputPressed(ImGuiNavInput_KeyMenu_, ImGuiInputReadMode_Released)) - if (IsMousePosValid(&g.IO.MousePos) == IsMousePosValid(&g.IO.MousePosPrev)) - apply_toggle_layer = true; - - // Move window - if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) - { - ImVec2 move_delta; - if (g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard && !g.IO.KeyShift) - move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); - if (g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) - move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down); - if (move_delta.x != 0.0f || move_delta.y != 0.0f) - { - const float NAV_MOVE_SPEED = 800.0f; - const float move_speed = ImFloor(NAV_MOVE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); - g.NavWindowingTarget->PosFloat += move_delta * move_speed; - g.NavDisableMouseHover = true; - MarkIniSettingsDirty(g.NavWindowingTarget); - } - } - - // Apply final focus - if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindowForTabbing)) - { - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; - apply_focus_window = NavRestoreLastChildNavWindow(apply_focus_window); - ClosePopupsOverWindow(apply_focus_window); - FocusWindow(apply_focus_window); - if (apply_focus_window->NavLastIds[0] == 0) - NavInitWindow(apply_focus_window, false); - - // If the window only has a menu layer, select it directly - if (apply_focus_window->DC.NavLayerActiveMask == (1 << 1)) - g.NavLayer = 1; - } - if (apply_focus_window) - g.NavWindowingTarget = NULL; - - // Apply menu/layer toggle - if (apply_toggle_layer && g.NavWindow) - { - ImGuiWindow *new_nav_window = g.NavWindow; - while ((new_nav_window->DC.NavLayerActiveMask & (1 << 1)) == 0 && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) - new_nav_window = new_nav_window->ParentWindow; - if (new_nav_window != g.NavWindow) - { - ImGuiWindow *old_nav_window = g.NavWindow; - FocusWindow(new_nav_window); - new_nav_window->NavLastChildNavWindow = old_nav_window; - } - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; - NavRestoreLayer((g.NavWindow->DC.NavLayerActiveMask & (1 << 1)) ? (g.NavLayer ^ 1) : 0); - } -} - -// NB: We modify rect_rel by the amount we scrolled for, so it is immediately updated. -static void NavScrollToBringItemIntoView(ImGuiWindow *window, ImRect &item_rect_rel) -{ - // Scroll to keep newly navigated item fully into view - ImRect window_rect_rel(window->InnerRect.Min - window->Pos - ImVec2(1, 1), window->InnerRect.Max - window->Pos + ImVec2(1, 1)); - // g.OverlayDrawList.AddRect(window->Pos + window_rect_rel.Min, window->Pos + window_rect_rel.Max, IM_COL32_WHITE); // [DEBUG] - if (window_rect_rel.Contains(item_rect_rel)) - return; - - ImGuiContext &g = *GImGui; - if (window->ScrollbarX && item_rect_rel.Min.x < window_rect_rel.Min.x) - { - window->ScrollTarget.x = item_rect_rel.Min.x + window->Scroll.x - g.Style.ItemSpacing.x; - window->ScrollTargetCenterRatio.x = 0.0f; - } - else if (window->ScrollbarX && item_rect_rel.Max.x >= window_rect_rel.Max.x) - { - window->ScrollTarget.x = item_rect_rel.Max.x + window->Scroll.x + g.Style.ItemSpacing.x; - window->ScrollTargetCenterRatio.x = 1.0f; - } - if (item_rect_rel.Min.y < window_rect_rel.Min.y) - { - window->ScrollTarget.y = item_rect_rel.Min.y + window->Scroll.y - g.Style.ItemSpacing.y; - window->ScrollTargetCenterRatio.y = 0.0f; - } - else if (item_rect_rel.Max.y >= window_rect_rel.Max.y) - { - window->ScrollTarget.y = item_rect_rel.Max.y + window->Scroll.y + g.Style.ItemSpacing.y; - window->ScrollTargetCenterRatio.y = 1.0f; - } - - // Estimate upcoming scroll so we can offset our relative mouse position so mouse position can be applied immediately (under this block) - ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); - item_rect_rel.Translate(window->Scroll - next_scroll); + const char* text_display_end = text; + if (!text_end) + text_end = (const char*)-1; + + while (text_display_end < text_end && *text_display_end != '\0' && (text_display_end[0] != '#' || text_display_end[1] != '#')) + text_display_end++; + return text_display_end; } -static void ImGui::NavUpdate() +// Internal ImGui functions to render text +// RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText() +void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) { - ImGuiContext &g = *GImGui; - g.IO.WantMoveMouse = false; - -#if 0 - if (g.NavScoringCount > 0) printf("[%05d] NavScoringCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.FrameCount, g.NavScoringCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); -#endif + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; - // Update Keyboard->Nav inputs mapping - memset(g.IO.NavInputs + ImGuiNavInput_InternalStart_, 0, (ImGuiNavInput_COUNT - ImGuiNavInput_InternalStart_) * sizeof(g.IO.NavInputs[0])); - if (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) - { -#define NAV_MAP_KEY(_KEY, _NAV_INPUT) \ - if (g.IO.KeyMap[_KEY] != -1 && IsKeyDown(g.IO.KeyMap[_KEY])) \ - g.IO.NavInputs[_NAV_INPUT] = 1.0f; - NAV_MAP_KEY(ImGuiKey_Space, ImGuiNavInput_Activate); - NAV_MAP_KEY(ImGuiKey_Enter, ImGuiNavInput_Input); - NAV_MAP_KEY(ImGuiKey_Escape, ImGuiNavInput_Cancel); - NAV_MAP_KEY(ImGuiKey_LeftArrow, ImGuiNavInput_KeyLeft_); - NAV_MAP_KEY(ImGuiKey_RightArrow, ImGuiNavInput_KeyRight_); - NAV_MAP_KEY(ImGuiKey_UpArrow, ImGuiNavInput_KeyUp_); - NAV_MAP_KEY(ImGuiKey_DownArrow, ImGuiNavInput_KeyDown_); - if (g.IO.KeyCtrl) - g.IO.NavInputs[ImGuiNavInput_TweakSlow] = 1.0f; - if (g.IO.KeyShift) - g.IO.NavInputs[ImGuiNavInput_TweakFast] = 1.0f; - if (g.IO.KeyAlt) - g.IO.NavInputs[ImGuiNavInput_KeyMenu_] = 1.0f; -#undef NAV_MAP_KEY - } - - memcpy(g.IO.NavInputsDownDurationPrev, g.IO.NavInputsDownDuration, sizeof(g.IO.NavInputsDownDuration)); - for (int i = 0; i < IM_ARRAYSIZE(g.IO.NavInputs); i++) - g.IO.NavInputsDownDuration[i] = (g.IO.NavInputs[i] > 0.0f) ? (g.IO.NavInputsDownDuration[i] < 0.0f ? 0.0f : g.IO.NavInputsDownDuration[i] + g.IO.DeltaTime) : -1.0f; - - // Process navigation init request (select first/default focus) - if (g.NavInitResultId != 0 && (!g.NavDisableHighlight || g.NavInitRequestFromMove)) - { - // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) - IM_ASSERT(g.NavWindow); - if (g.NavInitRequestFromMove) - SetNavIDAndMoveMouse(g.NavInitResultId, g.NavLayer, g.NavInitResultRectRel); - else - SetNavID(g.NavInitResultId, g.NavLayer); - g.NavWindow->NavRectRel[g.NavLayer] = g.NavInitResultRectRel; - } - g.NavInitRequest = false; - g.NavInitRequestFromMove = false; - g.NavInitResultId = 0; - g.NavJustMovedToId = 0; - - // Process navigation move request - if (g.NavMoveRequest && (g.NavMoveResultLocal.ID != 0 || g.NavMoveResultOther.ID != 0)) - { - // Select which result to use - ImGuiNavMoveResult *result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; - if (g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) // Maybe entering a flattened child? In this case solve the tie using the regular scoring rules - if ((g.NavMoveResultOther.DistBox < g.NavMoveResultLocal.DistBox) || (g.NavMoveResultOther.DistBox == g.NavMoveResultLocal.DistBox && g.NavMoveResultOther.DistCenter < g.NavMoveResultLocal.DistCenter)) - result = &g.NavMoveResultOther; - - IM_ASSERT(g.NavWindow && result->Window); - - // Scroll to keep newly navigated item fully into view - if (g.NavLayer == 0) - NavScrollToBringItemIntoView(result->Window, result->RectRel); - - // Apply result from previous frame navigation directional move request - ClearActiveID(); - g.NavWindow = result->Window; - SetNavIDAndMoveMouse(result->ID, g.NavLayer, result->RectRel); - g.NavJustMovedToId = result->ID; - g.NavMoveFromClampedRefRect = false; - } - - // When a forwarded move request failed, we restore the highlight that we disabled during the forward frame - if (g.NavMoveRequestForward == ImGuiNavForward_ForwardActive) - { - IM_ASSERT(g.NavMoveRequest); - if (g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0) - g.NavDisableHighlight = false; - g.NavMoveRequestForward = ImGuiNavForward_None; - } - - // Apply application mouse position movement, after we had a chance to process move request result. - if (g.NavMousePosDirty && g.NavIdIsAlive) - { - // Set mouse position given our knowledge of the nav widget position from last frame - if (g.IO.NavFlags & ImGuiNavFlags_MoveMouse) - { - g.IO.MousePos = g.IO.MousePosPrev = NavCalcPreferredMousePos(); - g.IO.WantMoveMouse = true; - } - g.NavMousePosDirty = false; - } - g.NavIdIsAlive = false; - g.NavJustTabbedId = 0; - IM_ASSERT(g.NavLayer == 0 || g.NavLayer == 1); - - // Store our return window (for returning from Layer 1 to Layer 0) and clear it as soon as we step back in our own Layer 0 - if (g.NavWindow) - NavSaveLastChildNavWindow(g.NavWindow); - if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == 0) - g.NavWindow->NavLastChildNavWindow = NULL; - - NavUpdateWindowing(); - - // Set output flags for user application - g.IO.NavActive = (g.IO.NavFlags & (ImGuiNavFlags_EnableGamepad | ImGuiNavFlags_EnableKeyboard)) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); - g.IO.NavVisible = (g.IO.NavActive && g.NavId != 0 && !g.NavDisableHighlight) || (g.NavWindowingTarget != NULL) || g.NavInitRequest; - - // Process NavCancel input (to close a popup, get back to parent, clear focus) - if (IsNavInputPressed(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed)) - { - if (g.ActiveId != 0) - { - ClearActiveID(); - } - else if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow) && !(g.NavWindow->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->ParentWindow) - { - // Exit child window - ImGuiWindow *child_window = g.NavWindow; - ImGuiWindow *parent_window = g.NavWindow->ParentWindow; - IM_ASSERT(child_window->ChildId != 0); - FocusWindow(parent_window); - SetNavID(child_window->ChildId, 0); - g.NavIdIsAlive = false; - if (g.NavDisableMouseHover) - g.NavMousePosDirty = true; - } - else if (g.OpenPopupStack.Size > 0) - { - // Close open popup/menu - if (!(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) - ClosePopupToLevel(g.OpenPopupStack.Size - 1); - } - else if (g.NavLayer != 0) - { - // Leave the "menu" layer - NavRestoreLayer(0); - } - else - { - // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were - if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup) || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) - g.NavWindow->NavLastIds[0] = 0; - g.NavId = 0; - } - } - - // Process manual activation request - g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = 0; - if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) - { - bool activate_down = IsNavInputDown(ImGuiNavInput_Activate); - bool activate_pressed = activate_down && IsNavInputPressed(ImGuiNavInput_Activate, ImGuiInputReadMode_Pressed); - if (g.ActiveId == 0 && activate_pressed) - g.NavActivateId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_down) - g.NavActivateDownId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_pressed) - g.NavActivatePressedId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && IsNavInputPressed(ImGuiNavInput_Input, ImGuiInputReadMode_Pressed)) - g.NavInputId = g.NavId; - } - if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) - g.NavDisableHighlight = true; - if (g.NavActivateId != 0) - IM_ASSERT(g.NavActivateDownId == g.NavActivateId); - g.NavMoveRequest = false; - - // Process programmatic activation request - if (g.NavNextActivateId != 0) - g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = g.NavNextActivateId; - g.NavNextActivateId = 0; - - // Initiate directional inputs request - const int allowed_dir_flags = (g.ActiveId == 0) ? ~0 : g.ActiveIdAllowNavDirFlags; - if (g.NavMoveRequestForward == ImGuiNavForward_None) - { - g.NavMoveDir = ImGuiDir_None; - if (g.NavWindow && !g.NavWindowingTarget && allowed_dir_flags && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) - { - if ((allowed_dir_flags & (1 << ImGuiDir_Left)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadLeft, ImGuiNavInput_KeyLeft_, ImGuiInputReadMode_Repeat)) - g.NavMoveDir = ImGuiDir_Left; - if ((allowed_dir_flags & (1 << ImGuiDir_Right)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadRight, ImGuiNavInput_KeyRight_, ImGuiInputReadMode_Repeat)) - g.NavMoveDir = ImGuiDir_Right; - if ((allowed_dir_flags & (1 << ImGuiDir_Up)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadUp, ImGuiNavInput_KeyUp_, ImGuiInputReadMode_Repeat)) - g.NavMoveDir = ImGuiDir_Up; - if ((allowed_dir_flags & (1 << ImGuiDir_Down)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadDown, ImGuiNavInput_KeyDown_, ImGuiInputReadMode_Repeat)) - g.NavMoveDir = ImGuiDir_Down; - } - } - else - { - // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) - IM_ASSERT(g.NavMoveDir != ImGuiDir_None); - IM_ASSERT(g.NavMoveRequestForward == ImGuiNavForward_ForwardQueued); - g.NavMoveRequestForward = ImGuiNavForward_ForwardActive; - } - - if (g.NavMoveDir != ImGuiDir_None) - { - g.NavMoveRequest = true; - g.NavMoveDirLast = g.NavMoveDir; - } - - // If we initiate a movement request and have no current NavId, we initiate a InitDefautRequest that will be used as a fallback if the direction fails to find a match - if (g.NavMoveRequest && g.NavId == 0) - { - g.NavInitRequest = g.NavInitRequestFromMove = true; - g.NavInitResultId = 0; - g.NavDisableHighlight = false; - } - - NavUpdateAnyRequestFlag(); - - // Scrolling - if (g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) - { - // *Fallback* manual-scroll with NavUp/NavDown when window has no navigable item - ImGuiWindow *window = g.NavWindow; - const float scroll_speed = ImFloor(window->CalcFontSize() * 100 * g.IO.DeltaTime + 0.5f); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. - if (window->DC.NavLayerActiveMask == 0x00 && window->DC.NavHasScroll && g.NavMoveRequest) - { - if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) - SetWindowScrollX(window, ImFloor(window->Scroll.x + ((g.NavMoveDir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); - if (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) - SetWindowScrollY(window, ImFloor(window->Scroll.y + ((g.NavMoveDir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); - } - - // *Normal* Manual scroll with NavScrollXXX keys - // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. - ImVec2 scroll_dir = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down, 1.0f / 10.0f, 10.0f); - if (scroll_dir.x != 0.0f && window->ScrollbarX) - { - SetWindowScrollX(window, ImFloor(window->Scroll.x + scroll_dir.x * scroll_speed)); - g.NavMoveFromClampedRefRect = true; - } - if (scroll_dir.y != 0.0f) - { - SetWindowScrollY(window, ImFloor(window->Scroll.y + scroll_dir.y * scroll_speed)); - g.NavMoveFromClampedRefRect = true; - } - } - - // Reset search results - g.NavMoveResultLocal.Clear(); - g.NavMoveResultOther.Clear(); - - // When we have manually scrolled (without using navigation) and NavId becomes out of bounds, we project its bounding box to the visible area to restart navigation within visible items - if (g.NavMoveRequest && g.NavMoveFromClampedRefRect && g.NavLayer == 0) - { - ImGuiWindow *window = g.NavWindow; - ImRect window_rect_rel(window->InnerRect.Min - window->Pos - ImVec2(1, 1), window->InnerRect.Max - window->Pos + ImVec2(1, 1)); - if (!window_rect_rel.Contains(window->NavRectRel[g.NavLayer])) - { - float pad = window->CalcFontSize() * 0.5f; - window_rect_rel.Expand(ImVec2(-ImMin(window_rect_rel.GetWidth(), pad), -ImMin(window_rect_rel.GetHeight(), pad))); // Terrible approximation for the intent of starting navigation from first fully visible item - window->NavRectRel[g.NavLayer].ClipWith(window_rect_rel); - g.NavId = 0; - } - g.NavMoveFromClampedRefRect = false; - } - - // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) - ImRect nav_rect_rel = (g.NavWindow && g.NavWindow->NavRectRel[g.NavLayer].IsFinite()) ? g.NavWindow->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0); - g.NavScoringRectScreen = g.NavWindow ? ImRect(g.NavWindow->Pos + nav_rect_rel.Min, g.NavWindow->Pos + nav_rect_rel.Max) : GetViewportRect(); - g.NavScoringRectScreen.Min.x = ImMin(g.NavScoringRectScreen.Min.x + 1.0f, g.NavScoringRectScreen.Max.x); - g.NavScoringRectScreen.Max.x = g.NavScoringRectScreen.Min.x; - IM_ASSERT(!g.NavScoringRectScreen.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous fabsf() calls in NavScoreItem(). - // g.OverlayDrawList.AddRect(g.NavScoringRectScreen.Min, g.NavScoringRectScreen.Max, IM_COL32(255,200,0,255)); // [DEBUG] - g.NavScoringCount = 0; -#if IMGUI_DEBUG_NAV_RECTS - if (g.NavWindow) - { - for (int layer = 0; layer < 2; layer++) - g.OverlayDrawList.AddRect(g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Min, g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Max, IM_COL32(255, 200, 0, 255)); - } // [DEBUG] - if (g.NavWindow) - { - ImU32 col = (g.NavWindow->HiddenFrames <= 0) ? IM_COL32(255, 0, 255, 255) : IM_COL32(255, 0, 0, 255); - ImVec2 p = NavCalcPreferredMousePos(); - char buf[32]; - ImFormatString(buf, 32, "%d", g.NavLayer); - g.OverlayDrawList.AddCircleFilled(p, 3.0f, col); - g.OverlayDrawList.AddText(NULL, 13.0f, p + ImVec2(8, -4), col, buf); - } -#endif -} + // Hide anything after a '##' string + const char* text_display_end; + if (hide_text_after_hash) + { + text_display_end = FindRenderedTextEnd(text, text_end); + } + else + { + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + text_display_end = text_end; + } -static void ImGui::UpdateMovingWindow() -{ - ImGuiContext &g = *GImGui; - if (g.MovingWindow && g.MovingWindow->MoveId == g.ActiveId && g.ActiveIdSource == ImGuiInputSource_Mouse) - { - // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). - // We track it to preserve Focus and so that ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. - KeepAliveID(g.ActiveId); - IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); - ImGuiWindow *moving_window = g.MovingWindow->RootWindow; - if (g.IO.MouseDown[0]) - { - ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; - if (moving_window->PosFloat.x != pos.x || moving_window->PosFloat.y != pos.y) - { - MarkIniSettingsDirty(moving_window); - moving_window->PosFloat = pos; - } - FocusWindow(g.MovingWindow); - } - else - { - ClearActiveID(); - g.MovingWindow = NULL; - } - } - else - { - // When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others. - if (g.ActiveIdWindow && g.ActiveIdWindow->MoveId == g.ActiveId) - { - KeepAliveID(g.ActiveId); - if (!g.IO.MouseDown[0]) - ClearActiveID(); - } - g.MovingWindow = NULL; - } + if (text != text_display_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_display_end); + } } -void ImGui::NewFrame() +void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width) { - IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); - ImGuiContext &g = *GImGui; - - // Check user data - // (We pass an error message in the assert expression as a trick to get it visible to programmers who are not using a debugger, as most assert handlers display their argument) - IM_ASSERT(g.Initialized); - IM_ASSERT(g.IO.DeltaTime >= 0.0f && "Need a positive DeltaTime (zero is tolerated but will cause some timing issues)"); - IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value"); - IM_ASSERT(g.IO.Fonts->Fonts.Size > 0 && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); - IM_ASSERT(g.IO.Fonts->Fonts[0]->IsLoaded() && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); - IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting"); - IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting. Alpha cannot be negative (allows us to avoid a few clamps in color computations)"); - IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); - for (int n = 0; n < ImGuiKey_COUNT; n++) - IM_ASSERT(g.IO.KeyMap[n] >= -1 && g.IO.KeyMap[n] < IM_ARRAYSIZE(g.IO.KeysDown) && "io.KeyMap[] contains an out of bound value (need to be 0..512, or -1 for unmapped key)"); - - // Do a simple check for required key mapping (we intentionally do NOT check all keys to not pressure user into setting up everything, but Space is required and was super recently added in 1.60 WIP) - if (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) - IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); - - // Load settings on first frame - if (!g.SettingsLoaded) - { - IM_ASSERT(g.SettingsWindows.empty()); - LoadIniSettingsFromDisk(g.IO.IniFilename); - g.SettingsLoaded = true; - } - - g.Time += g.IO.DeltaTime; - g.FrameCount += 1; - g.TooltipOverrideCount = 0; - g.WindowsActiveCount = 0; - - SetCurrentFont(GetDefaultFont()); - IM_ASSERT(g.Font->IsLoaded()); - g.DrawListSharedData.ClipRectFullscreen = ImVec4(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); - g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; - - g.OverlayDrawList.Clear(); - g.OverlayDrawList.PushTextureID(g.IO.Fonts->TexID); - g.OverlayDrawList.PushClipRectFullScreen(); - g.OverlayDrawList.Flags = (g.Style.AntiAliasedLines ? ImDrawListFlags_AntiAliasedLines : 0) | (g.Style.AntiAliasedFill ? ImDrawListFlags_AntiAliasedFill : 0); - - // Mark rendering data as invalid to prevent user who may have a handle on it to use it - g.DrawData.Clear(); - - // Clear reference to active widget if the widget isn't alive anymore - if (!g.HoveredIdPreviousFrame) - g.HoveredIdTimer = 0.0f; - g.HoveredIdPreviousFrame = g.HoveredId; - g.HoveredId = 0; - g.HoveredIdAllowOverlap = false; - if (!g.ActiveIdIsAlive && g.ActiveIdPreviousFrame == g.ActiveId && g.ActiveId != 0) - ClearActiveID(); - if (g.ActiveId) - g.ActiveIdTimer += g.IO.DeltaTime; - g.ActiveIdPreviousFrame = g.ActiveId; - g.ActiveIdIsAlive = false; - g.ActiveIdIsJustActivated = false; - if (g.ScalarAsInputTextId && g.ActiveId != g.ScalarAsInputTextId) - g.ScalarAsInputTextId = 0; - - // Elapse drag & drop payload - if (g.DragDropActive && g.DragDropPayload.DataFrameCount + 1 < g.FrameCount) - { - ClearDragDrop(); - g.DragDropPayloadBufHeap.clear(); - memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); - } - g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; - g.DragDropAcceptIdCurr = 0; - g.DragDropAcceptIdCurrRectSurface = FLT_MAX; - - // Update keyboard input state - memcpy(g.IO.KeysDownDurationPrev, g.IO.KeysDownDuration, sizeof(g.IO.KeysDownDuration)); - for (int i = 0; i < IM_ARRAYSIZE(g.IO.KeysDown); i++) - g.IO.KeysDownDuration[i] = g.IO.KeysDown[i] ? (g.IO.KeysDownDuration[i] < 0.0f ? 0.0f : g.IO.KeysDownDuration[i] + g.IO.DeltaTime) : -1.0f; - - // Update gamepad/keyboard directional navigation - NavUpdate(); - - // Update mouse input state - // If mouse just appeared or disappeared (usually denoted by -FLT_MAX component, but in reality we test for -256000.0f) we cancel out movement in MouseDelta - if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MousePosPrev)) - g.IO.MouseDelta = g.IO.MousePos - g.IO.MousePosPrev; - else - g.IO.MouseDelta = ImVec2(0.0f, 0.0f); - if (g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f) - g.NavDisableMouseHover = false; - - g.IO.MousePosPrev = g.IO.MousePos; - for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) - { - g.IO.MouseClicked[i] = g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] < 0.0f; - g.IO.MouseReleased[i] = !g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] >= 0.0f; - g.IO.MouseDownDurationPrev[i] = g.IO.MouseDownDuration[i]; - g.IO.MouseDownDuration[i] = g.IO.MouseDown[i] ? (g.IO.MouseDownDuration[i] < 0.0f ? 0.0f : g.IO.MouseDownDuration[i] + g.IO.DeltaTime) : -1.0f; - g.IO.MouseDoubleClicked[i] = false; - if (g.IO.MouseClicked[i]) - { - if (g.Time - g.IO.MouseClickedTime[i] < g.IO.MouseDoubleClickTime) - { - if (ImLengthSqr(g.IO.MousePos - g.IO.MouseClickedPos[i]) < g.IO.MouseDoubleClickMaxDist * g.IO.MouseDoubleClickMaxDist) - g.IO.MouseDoubleClicked[i] = true; - g.IO.MouseClickedTime[i] = -FLT_MAX; // so the third click isn't turned into a double-click - } - else - { - g.IO.MouseClickedTime[i] = g.Time; - } - g.IO.MouseClickedPos[i] = g.IO.MousePos; - g.IO.MouseDragMaxDistanceAbs[i] = ImVec2(0.0f, 0.0f); - g.IO.MouseDragMaxDistanceSqr[i] = 0.0f; - } - else if (g.IO.MouseDown[i]) - { - ImVec2 mouse_delta = g.IO.MousePos - g.IO.MouseClickedPos[i]; - g.IO.MouseDragMaxDistanceAbs[i].x = ImMax(g.IO.MouseDragMaxDistanceAbs[i].x, mouse_delta.x < 0.0f ? -mouse_delta.x : mouse_delta.x); - g.IO.MouseDragMaxDistanceAbs[i].y = ImMax(g.IO.MouseDragMaxDistanceAbs[i].y, mouse_delta.y < 0.0f ? -mouse_delta.y : mouse_delta.y); - g.IO.MouseDragMaxDistanceSqr[i] = ImMax(g.IO.MouseDragMaxDistanceSqr[i], ImLengthSqr(mouse_delta)); - } - if (g.IO.MouseClicked[i]) // Clicking any mouse button reactivate mouse hovering which may have been deactivated by gamepad/keyboard navigation - g.NavDisableMouseHover = false; - } - - // Calculate frame-rate for the user, as a purely luxurious feature - g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; - g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); - g.IO.Framerate = 1.0f / (g.FramerateSecPerFrameAccum / (float) IM_ARRAYSIZE(g.FramerateSecPerFrame)); - - // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) - UpdateMovingWindow(); - - // Delay saving settings so we don't spam disk too much - if (g.SettingsDirtyTimer > 0.0f) - { - g.SettingsDirtyTimer -= g.IO.DeltaTime; - if (g.SettingsDirtyTimer <= 0.0f) - SaveIniSettingsToDisk(g.IO.IniFilename); - } - - // Find the window we are hovering - // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. - // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point. - // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. - g.HoveredWindow = (g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoInputs)) ? g.MovingWindow : FindHoveredWindow(); - g.HoveredRootWindow = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; - - ImGuiWindow *modal_window = GetFrontMostModalRootWindow(); - if (modal_window != NULL) - { - g.ModalWindowDarkeningRatio = ImMin(g.ModalWindowDarkeningRatio + g.IO.DeltaTime * 6.0f, 1.0f); - if (g.HoveredRootWindow && !IsWindowChildOf(g.HoveredRootWindow, modal_window)) - g.HoveredRootWindow = g.HoveredWindow = NULL; - } - else - { - g.ModalWindowDarkeningRatio = 0.0f; - } - - // Update the WantCaptureMouse/WantCaptureKeyboard flags, so user can capture/discard the inputs away from the rest of their application. - // When clicking outside of a window we assume the click is owned by the application and won't request capture. We need to track click ownership. - int mouse_earliest_button_down = -1; - bool mouse_any_down = false; - for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) - { - if (g.IO.MouseClicked[i]) - g.IO.MouseDownOwned[i] = (g.HoveredWindow != NULL) || (!g.OpenPopupStack.empty()); - mouse_any_down |= g.IO.MouseDown[i]; - if (g.IO.MouseDown[i]) - if (mouse_earliest_button_down == -1 || g.IO.MouseClickedTime[i] < g.IO.MouseClickedTime[mouse_earliest_button_down]) - mouse_earliest_button_down = i; - } - bool mouse_avail_to_imgui = (mouse_earliest_button_down == -1) || g.IO.MouseDownOwned[mouse_earliest_button_down]; - if (g.WantCaptureMouseNextFrame != -1) - g.IO.WantCaptureMouse = (g.WantCaptureMouseNextFrame != 0); - else - g.IO.WantCaptureMouse = (mouse_avail_to_imgui && (g.HoveredWindow != NULL || mouse_any_down)) || (!g.OpenPopupStack.empty()); - - if (g.WantCaptureKeyboardNextFrame != -1) - g.IO.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); - else - g.IO.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); - if (g.IO.NavActive && (g.IO.NavFlags & ImGuiNavFlags_EnableKeyboard) && !(g.IO.NavFlags & ImGuiNavFlags_NoCaptureKeyboard)) - g.IO.WantCaptureKeyboard = true; - - g.IO.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : 0; - g.MouseCursor = ImGuiMouseCursor_Arrow; - g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; - g.OsImePosRequest = ImVec2(1.0f, 1.0f); // OS Input Method Editor showing on top-left of our window by default - - // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. - // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) - bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; - if (!mouse_avail_to_imgui && !mouse_dragging_extern_payload) - g.HoveredWindow = g.HoveredRootWindow = NULL; - - // Mouse wheel scrolling, scale - if (g.HoveredWindow && !g.HoveredWindow->Collapsed && (g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f)) - { - // If a child window has the ImGuiWindowFlags_NoScrollWithMouse flag, we give a chance to scroll its parent (unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set). - ImGuiWindow *window = g.HoveredWindow; - ImGuiWindow *scroll_window = window; - while ((scroll_window->Flags & ImGuiWindowFlags_ChildWindow) && (scroll_window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(scroll_window->Flags & ImGuiWindowFlags_NoScrollbar) && !(scroll_window->Flags & ImGuiWindowFlags_NoInputs) && scroll_window->ParentWindow) - scroll_window = scroll_window->ParentWindow; - const bool scroll_allowed = !(scroll_window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(scroll_window->Flags & ImGuiWindowFlags_NoInputs); - - if (g.IO.MouseWheel != 0.0f) - { - if (g.IO.KeyCtrl && g.IO.FontAllowUserScaling) - { - // Zoom / Scale window - const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); - const float scale = new_font_scale / window->FontWindowScale; - window->FontWindowScale = new_font_scale; - - const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; - window->Pos += offset; - window->PosFloat += offset; - window->Size *= scale; - window->SizeFull *= scale; - } - else if (!g.IO.KeyCtrl && scroll_allowed) - { - // Mouse wheel vertical scrolling - float scroll_amount = 5 * scroll_window->CalcFontSize(); - scroll_amount = (float) (int) ImMin(scroll_amount, (scroll_window->ContentsRegionRect.GetHeight() + scroll_window->WindowPadding.y * 2.0f) * 0.67f); - SetWindowScrollY(scroll_window, scroll_window->Scroll.y - g.IO.MouseWheel * scroll_amount); - } - } - if (g.IO.MouseWheelH != 0.0f && scroll_allowed) - { - // Mouse wheel horizontal scrolling (for hardware that supports it) - float scroll_amount = scroll_window->CalcFontSize(); - if (!g.IO.KeyCtrl && !(window->Flags & ImGuiWindowFlags_NoScrollWithMouse)) - SetWindowScrollX(window, window->Scroll.x - g.IO.MouseWheelH * scroll_amount); - } - } - - // Pressing TAB activate widget focus - if (g.ActiveId == 0 && g.NavWindow != NULL && g.NavWindow->Active && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab, false)) - { - if (g.NavId != 0 && g.NavIdTabCounter != INT_MAX) - g.NavWindow->FocusIdxTabRequestNext = g.NavIdTabCounter + 1 + (g.IO.KeyShift ? -1 : 1); - else - g.NavWindow->FocusIdxTabRequestNext = g.IO.KeyShift ? -1 : 0; - } - g.NavIdTabCounter = INT_MAX; - - // Mark all windows as not visible - for (int i = 0; i != g.Windows.Size; i++) - { - ImGuiWindow *window = g.Windows[i]; - window->WasActive = window->Active; - window->Active = false; - window->WriteAccessed = false; - } - - // Closing the focused window restore focus to the first active root window in descending z-order - if (g.NavWindow && !g.NavWindow->WasActive) - FocusFrontMostActiveWindow(NULL); - - // No window should be open at the beginning of the frame. - // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. - g.CurrentWindowStack.resize(0); - g.CurrentPopupStack.resize(0); - ClosePopupsOverWindow(g.NavWindow); - - // Create implicit window - we will only render it if the user has added something to it. - // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. - SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver); - Begin("Debug##Default"); -} - -static void *SettingsHandlerWindow_ReadOpen(ImGuiContext *, ImGuiSettingsHandler *, const char *name) -{ - ImGuiWindowSettings *settings = ImGui::FindWindowSettings(ImHash(name, 0)); - if (!settings) - settings = AddWindowSettings(name); - return (void *) settings; -} - -static void SettingsHandlerWindow_ReadLine(ImGuiContext *, ImGuiSettingsHandler *, void *entry, const char *line) -{ - ImGuiWindowSettings *settings = (ImGuiWindowSettings *) entry; - float x, y; - int i; - if (sscanf(line, "Pos=%f,%f", &x, &y) == 2) - settings->Pos = ImVec2(x, y); - else if (sscanf(line, "Size=%f,%f", &x, &y) == 2) - settings->Size = ImMax(ImVec2(x, y), GImGui->Style.WindowMinSize); - else if (sscanf(line, "Collapsed=%d", &i) == 1) - settings->Collapsed = (i != 0); -} - -static void SettingsHandlerWindow_WriteAll(ImGuiContext *imgui_ctx, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf) -{ - // Gather data from windows that were active during this session - ImGuiContext &g = *imgui_ctx; - for (int i = 0; i != g.Windows.Size; i++) - { - ImGuiWindow *window = g.Windows[i]; - if (window->Flags & ImGuiWindowFlags_NoSavedSettings) - continue; - ImGuiWindowSettings *settings = ImGui::FindWindowSettings(window->ID); - if (!settings) - settings = AddWindowSettings(window->Name); - settings->Pos = window->Pos; - settings->Size = window->SizeFull; - settings->Collapsed = window->Collapsed; - } - - // Write a buffer - // If a window wasn't opened in this session we preserve its settings - buf->reserve(buf->size() + g.SettingsWindows.Size * 96); // ballpark reserve - for (int i = 0; i != g.SettingsWindows.Size; i++) - { - const ImGuiWindowSettings *settings = &g.SettingsWindows[i]; - if (settings->Pos.x == FLT_MAX) - continue; - const char *name = settings->Name; - if (const char *p = strstr(name, "###")) // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() - name = p; - buf->appendf("[%s][%s]\n", handler->TypeName, name); - buf->appendf("Pos=%d,%d\n", (int) settings->Pos.x, (int) settings->Pos.y); - buf->appendf("Size=%d,%d\n", (int) settings->Size.x, (int) settings->Size.y); - buf->appendf("Collapsed=%d\n", settings->Collapsed); - buf->appendf("\n"); - } -} - -void ImGui::Initialize(ImGuiContext *context) -{ - ImGuiContext &g = *context; - IM_ASSERT(!g.Initialized && !g.SettingsLoaded); - g.LogClipboard = IM_NEW(ImGuiTextBuffer)(); - - // Add .ini handle for ImGuiWindow type - ImGuiSettingsHandler ini_handler; - ini_handler.TypeName = "Window"; - ini_handler.TypeHash = ImHash("Window", 0, 0); - ini_handler.ReadOpenFn = SettingsHandlerWindow_ReadOpen; - ini_handler.ReadLineFn = SettingsHandlerWindow_ReadLine; - ini_handler.WriteAllFn = SettingsHandlerWindow_WriteAll; - g.SettingsHandlers.push_front(ini_handler); - - g.Initialized = true; -} + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; -// This function is merely here to free heap allocations. -void ImGui::Shutdown(ImGuiContext *context) -{ - ImGuiContext &g = *context; - - // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) - if (g.IO.Fonts && g.FontAtlasOwnedByContext) - IM_DELETE(g.IO.Fonts); - - // Cleanup of other data are conditional on actually having initialize ImGui. - if (!g.Initialized) - return; - - SaveIniSettingsToDisk(g.IO.IniFilename); - - // Clear everything else - for (int i = 0; i < g.Windows.Size; i++) - IM_DELETE(g.Windows[i]); - g.Windows.clear(); - g.WindowsSortBuffer.clear(); - g.CurrentWindow = NULL; - g.CurrentWindowStack.clear(); - g.WindowsById.Clear(); - g.NavWindow = NULL; - g.HoveredWindow = NULL; - g.HoveredRootWindow = NULL; - g.ActiveIdWindow = NULL; - g.MovingWindow = NULL; - for (int i = 0; i < g.SettingsWindows.Size; i++) - IM_DELETE(g.SettingsWindows[i].Name); - g.ColorModifiers.clear(); - g.StyleModifiers.clear(); - g.FontStack.clear(); - g.OpenPopupStack.clear(); - g.CurrentPopupStack.clear(); - g.DrawDataBuilder.ClearFreeMemory(); - g.OverlayDrawList.ClearFreeMemory(); - g.PrivateClipboard.clear(); - g.InputTextState.Text.clear(); - g.InputTextState.InitialText.clear(); - g.InputTextState.TempTextBuffer.clear(); - - g.SettingsWindows.clear(); - g.SettingsHandlers.clear(); - - if (g.LogFile && g.LogFile != stdout) - { - fclose(g.LogFile); - g.LogFile = NULL; - } - if (g.LogClipboard) - IM_DELETE(g.LogClipboard); - - g.Initialized = false; -} - -ImGuiWindowSettings *ImGui::FindWindowSettings(ImGuiID id) -{ - ImGuiContext &g = *GImGui; - for (int i = 0; i != g.SettingsWindows.Size; i++) - if (g.SettingsWindows[i].Id == id) - return &g.SettingsWindows[i]; - return NULL; -} - -static ImGuiWindowSettings *AddWindowSettings(const char *name) -{ - ImGuiContext &g = *GImGui; - g.SettingsWindows.push_back(ImGuiWindowSettings()); - ImGuiWindowSettings *settings = &g.SettingsWindows.back(); - settings->Name = ImStrdup(name); - settings->Id = ImHash(name, 0); - return settings; -} - -static void LoadIniSettingsFromDisk(const char *ini_filename) -{ - if (!ini_filename) - return; - char *file_data = (char *) ImFileLoadToMemory(ini_filename, "rb", NULL, +1); - if (!file_data) - return; - LoadIniSettingsFromMemory(file_data); - ImGui::MemFree(file_data); -} - -ImGuiSettingsHandler *ImGui::FindSettingsHandler(const char *type_name) -{ - ImGuiContext &g = *GImGui; - const ImGuiID type_hash = ImHash(type_name, 0, 0); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].TypeHash == type_hash) - return &g.SettingsHandlers[handler_n]; - return NULL; -} + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT -// Zero-tolerance, no error reporting, cheap .ini parsing -static void LoadIniSettingsFromMemory(const char *buf_readonly) -{ - // For convenience and to make the code simpler, we'll write zero terminators inside the buffer. So let's create a writable copy. - char *buf = ImStrdup(buf_readonly); - char *buf_end = buf + strlen(buf); - - ImGuiContext &g = *GImGui; - void *entry_data = NULL; - ImGuiSettingsHandler *entry_handler = NULL; - - char *line_end = NULL; - for (char *line = buf; line < buf_end; line = line_end + 1) - { - // Skip new lines markers, then find end of the line - while (*line == '\n' || *line == '\r') - line++; - line_end = line; - while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') - line_end++; - line_end[0] = 0; - - if (line[0] == '[' && line_end > line && line_end[-1] == ']') - { - // Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code. - line_end[-1] = 0; - const char *name_end = line_end - 1; - const char *type_start = line + 1; - char *type_end = ImStrchrRange(type_start, name_end, ']'); - const char *name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; - if (!type_end || !name_start) - { - name_start = type_start; // Import legacy entries that have no type - type_start = "Window"; - } - else - { - *type_end = 0; // Overwrite first ']' - name_start++; // Skip second '[' - } - entry_handler = ImGui::FindSettingsHandler(type_start); - entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; - } - else if (entry_handler != NULL && entry_data != NULL) - { - // Let type handler parse the line - entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); - } - } - ImGui::MemFree(buf); - g.SettingsLoaded = true; -} - -static void SaveIniSettingsToDisk(const char *ini_filename) -{ - ImGuiContext &g = *GImGui; - g.SettingsDirtyTimer = 0.0f; - if (!ini_filename) - return; - - ImVector buf; - SaveIniSettingsToMemory(buf); - - FILE *f = ImFileOpen(ini_filename, "wt"); - if (!f) - return; - fwrite(buf.Data, sizeof(char), (size_t) buf.Size, f); - fclose(f); -} - -static void SaveIniSettingsToMemory(ImVector &out_buf) -{ - ImGuiContext &g = *GImGui; - g.SettingsDirtyTimer = 0.0f; - - ImGuiTextBuffer buf; - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - { - ImGuiSettingsHandler *handler = &g.SettingsHandlers[handler_n]; - handler->WriteAllFn(&g, handler, &buf); - } - - buf.Buf.pop_back(); // Remove extra zero-terminator used by ImGuiTextBuffer - out_buf.swap(buf.Buf); + if (text != text_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_end, wrap_width); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_end); + } } -void ImGui::MarkIniSettingsDirty() -{ - ImGuiContext &g = *GImGui; - if (g.SettingsDirtyTimer <= 0.0f) - g.SettingsDirtyTimer = g.IO.IniSavingRate; +// Default clip_rect uses (pos_min,pos_max) +// Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) +// FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especally for text above draw_list->DrawList. +// Effectively as this is called from widget doing their own coarse clipping it's not very valuable presently. Next time function will take +// better advantage of the render function taking size into account for coarse clipping. +void ImGui::RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Perform CPU side clipping for single clipped element to avoid using scissor state + ImVec2 pos = pos_min; + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f); + + const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min; + const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max; + bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); + if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min + need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); + + // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. + if (align.x > 0.0f) pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x); + if (align.y > 0.0f) pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y); + + // Render + if (need_clipping) + { + ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); + } + else + { + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); + } } -static void MarkIniSettingsDirty(ImGuiWindow *window) +void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) { - ImGuiContext &g = *GImGui; - if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) - if (g.SettingsDirtyTimer <= 0.0f) - g.SettingsDirtyTimer = g.IO.IniSavingRate; -} + // Hide anything after a '##' string + const char* text_display_end = FindRenderedTextEnd(text, text_end); + const int text_len = (int)(text_display_end - text); + if (text_len == 0) + return; -// FIXME: Add a more explicit sort order in the window structure. -static int IMGUI_CDECL ChildWindowComparer(const void *lhs, const void *rhs) -{ - const ImGuiWindow *a = *(const ImGuiWindow **) lhs; - const ImGuiWindow *b = *(const ImGuiWindow **) rhs; - if (int d = (a->Flags & ImGuiWindowFlags_Popup) - (b->Flags & ImGuiWindowFlags_Popup)) - return d; - if (int d = (a->Flags & ImGuiWindowFlags_Tooltip) - (b->Flags & ImGuiWindowFlags_Tooltip)) - return d; - return (a->BeginOrderWithinParent - b->BeginOrderWithinParent); -} - -static void AddWindowToSortedBuffer(ImVector *out_sorted_windows, ImGuiWindow *window) -{ - out_sorted_windows->push_back(window); - if (window->Active) - { - int count = window->DC.ChildWindows.Size; - if (count > 1) - qsort(window->DC.ChildWindows.begin(), (size_t) count, sizeof(ImGuiWindow *), ChildWindowComparer); - for (int i = 0; i < count; i++) - { - ImGuiWindow *child = window->DC.ChildWindows[i]; - if (child->Active) - AddWindowToSortedBuffer(out_sorted_windows, child); - } - } -} - -static void AddDrawListToDrawData(ImVector *out_render_list, ImDrawList *draw_list) -{ - if (draw_list->CmdBuffer.empty()) - return; - - // Remove trailing command if unused - ImDrawCmd &last_cmd = draw_list->CmdBuffer.back(); - if (last_cmd.ElemCount == 0 && last_cmd.UserCallback == NULL) - { - draw_list->CmdBuffer.pop_back(); - if (draw_list->CmdBuffer.empty()) - return; - } - - // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. May trigger for you if you are using PrimXXX functions incorrectly. - IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); - IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); - IM_ASSERT((int) draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); - - // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) - // If this assert triggers because you are drawing lots of stuff manually: - // A) Make sure you are coarse clipping, because ImDrawList let all your vertices pass. You can use the Metrics window to inspect draw list contents. - // B) If you need/want meshes with more than 64K vertices, uncomment the '#define ImDrawIdx unsigned int' line in imconfig.h to set the index size to 4 bytes. - // You'll need to handle the 4-bytes indices to your renderer. For example, the OpenGL example code detect index size at compile-time by doing: - // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); - // Your own engine or render API may use different parameters or function calls to specify index sizes. 2 and 4 bytes indices are generally supported by most API. - // C) If for some reason you cannot use 4 bytes indices or don't want to, a workaround is to call BeginChild()/EndChild() before reaching the 64K limit to split your draw commands in multiple draw lists. - if (sizeof(ImDrawIdx) == 2) - IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); - - out_render_list->push_back(draw_list); -} - -static void AddWindowToDrawData(ImVector *out_render_list, ImGuiWindow *window) -{ - AddDrawListToDrawData(out_render_list, window->DrawList); - for (int i = 0; i < window->DC.ChildWindows.Size; i++) - { - ImGuiWindow *child = window->DC.ChildWindows[i]; - if (child->Active && child->HiddenFrames <= 0) // clipped children may have been marked not active - AddWindowToDrawData(out_render_list, child); - } -} - -static void AddWindowToDrawDataSelectLayer(ImGuiWindow *window) -{ - ImGuiContext &g = *GImGui; - g.IO.MetricsActiveWindows++; - if (window->Flags & ImGuiWindowFlags_Tooltip) - AddWindowToDrawData(&g.DrawDataBuilder.Layers[1], window); - else - AddWindowToDrawData(&g.DrawDataBuilder.Layers[0], window); -} - -void ImDrawDataBuilder::FlattenIntoSingleLayer() -{ - int n = Layers[0].Size; - int size = n; - for (int i = 1; i < IM_ARRAYSIZE(Layers); i++) - size += Layers[i].Size; - Layers[0].resize(size); - for (int layer_n = 1; layer_n < IM_ARRAYSIZE(Layers); layer_n++) - { - ImVector &layer = Layers[layer_n]; - if (layer.empty()) - continue; - memcpy(&Layers[0][n], &layer[0], layer.Size * sizeof(ImDrawList *)); - n += layer.Size; - layer.resize(0); - } -} - -static void SetupDrawData(ImVector *draw_lists, ImDrawData *out_draw_data) -{ - out_draw_data->Valid = true; - out_draw_data->CmdLists = (draw_lists->Size > 0) ? draw_lists->Data : NULL; - out_draw_data->CmdListsCount = draw_lists->Size; - out_draw_data->TotalVtxCount = out_draw_data->TotalIdxCount = 0; - for (int n = 0; n < draw_lists->Size; n++) - { - out_draw_data->TotalVtxCount += draw_lists->Data[n]->VtxBuffer.Size; - out_draw_data->TotalIdxCount += draw_lists->Data[n]->IdxBuffer.Size; - } -} - -// When using this function it is sane to ensure that float are perfectly rounded to integer values, to that e.g. (int)(max.x-min.x) in user's render produce correct result. -void ImGui::PushClipRect(const ImVec2 &clip_rect_min, const ImVec2 &clip_rect_max, bool intersect_with_current_clip_rect) -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); - window->ClipRect = window->DrawList->_ClipRectStack.back(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + RenderTextClippedEx(window->DrawList, pos_min, pos_max, text, text_display_end, text_size_if_known, align, clip_rect); + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_display_end); } -void ImGui::PopClipRect() +// Another overly complex function until we reorganize everything into a nice all-in-one helper. +// This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display. +// This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move. +void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known) { - ImGuiWindow *window = GetCurrentWindow(); - window->DrawList->PopClipRect(); - window->ClipRect = window->DrawList->_ClipRectStack.back(); + ImGuiContext& g = *GImGui; + if (text_end_full == NULL) + text_end_full = FindRenderedTextEnd(text); + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f); + + //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 4), IM_COL32(0, 0, 255, 255)); + //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y-2), ImVec2(ellipsis_max_x, pos_max.y+2), IM_COL32(0, 255, 0, 255)); + //draw_list->AddLine(ImVec2(clip_max_x, pos_min.y), ImVec2(clip_max_x, pos_max.y), IM_COL32(255, 0, 0, 255)); + // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels. + if (text_size.x > pos_max.x - pos_min.x) + { + // Hello wo... + // | | | + // min max ellipsis_max + // <-> this is generally some padding value + + const ImFont* font = draw_list->_Data->Font; + const float font_size = draw_list->_Data->FontSize; + const float font_scale = draw_list->_Data->FontScale; + const char* text_end_ellipsis = NULL; + const float ellipsis_width = font->EllipsisWidth * font_scale; + + // We can now claim the space between pos_max.x and ellipsis_max.x + const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_width) - pos_min.x, 1.0f); + float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x; + if (text == text_end_ellipsis && text_end_ellipsis < text_end_full) + { + // Always display at least 1 character if there's no room for character + ellipsis + text_end_ellipsis = text + ImTextCountUtf8BytesFromChar(text, text_end_full); + text_size_clipped_x = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text, text_end_ellipsis).x; + } + while (text_end_ellipsis > text && ImCharIsBlankA(text_end_ellipsis[-1])) + { + // Trim trailing space before ellipsis (FIXME: Supporting non-ascii blanks would be nice, for this we need a function to backtrack in UTF-8 text) + text_end_ellipsis--; + text_size_clipped_x -= font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text_end_ellipsis, text_end_ellipsis + 1).x; // Ascii blanks are always 1 byte + } + + // Render text, render ellipsis + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); + ImVec2 ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y)); + if (ellipsis_pos.x + ellipsis_width <= ellipsis_max_x) + for (int i = 0; i < font->EllipsisCharCount; i++, ellipsis_pos.x += font->EllipsisCharStep * font_scale) + font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar); + } + else + { + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_full, &text_size, ImVec2(0.0f, 0.0f)); + } + + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_end_full); } -// This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. -void ImGui::EndFrame() +// Render a rectangle shaped with optional rounding and borders +void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders, float rounding) { - ImGuiContext &g = *GImGui; - IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() - if (g.FrameCountEnded == g.FrameCount) // Don't process EndFrame() multiple times. - return; - - // Notify OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) - if (g.IO.ImeSetInputScreenPosFn && ImLengthSqr(g.OsImePosRequest - g.OsImePosSet) > 0.0001f) - { - g.IO.ImeSetInputScreenPosFn((int) g.OsImePosRequest.x, (int) g.OsImePosRequest.y); - g.OsImePosSet = g.OsImePosRequest; - } - - // Hide implicit "Debug" window if it hasn't been used - IM_ASSERT(g.CurrentWindowStack.Size == 1); // Mismatched Begin()/End() calls - if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) - g.CurrentWindow->Active = false; - End(); - - if (g.ActiveId == 0 && g.HoveredId == 0) - { - if (!g.NavWindow || !g.NavWindow->Appearing) // Unless we just made a window/popup appear - { - // Click to focus window and start moving (after we're done with all our widgets) - if (g.IO.MouseClicked[0]) - { - if (g.HoveredRootWindow != NULL) - { - // Set ActiveId even if the _NoMove flag is set, without it dragging away from a window with _NoMove would activate hover on other windows. - FocusWindow(g.HoveredWindow); - SetActiveID(g.HoveredWindow->MoveId, g.HoveredWindow); - g.NavDisableHighlight = true; - g.ActiveIdClickOffset = g.IO.MousePos - g.HoveredRootWindow->Pos; - if (!(g.HoveredWindow->Flags & ImGuiWindowFlags_NoMove) && !(g.HoveredRootWindow->Flags & ImGuiWindowFlags_NoMove)) - g.MovingWindow = g.HoveredWindow; - } - else if (g.NavWindow != NULL && GetFrontMostModalRootWindow() == NULL) - { - // Clicking on void disable focus - FocusWindow(NULL); - } - } - - // With right mouse button we close popups without changing focus - // (The left mouse button path calls FocusWindow which will lead NewFrame->ClosePopupsOverWindow to trigger) - if (g.IO.MouseClicked[1]) - { - // Find the top-most window between HoveredWindow and the front most Modal Window. - // This is where we can trim the popup stack. - ImGuiWindow *modal = GetFrontMostModalRootWindow(); - bool hovered_window_above_modal = false; - if (modal == NULL) - hovered_window_above_modal = true; - for (int i = g.Windows.Size - 1; i >= 0 && hovered_window_above_modal == false; i--) - { - ImGuiWindow *window = g.Windows[i]; - if (window == modal) - break; - if (window == g.HoveredWindow) - hovered_window_above_modal = true; - } - ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal); - } - } - } - - // Sort the window list so that all child windows are after their parent - // We cannot do that on FocusWindow() because childs may not exist yet - g.WindowsSortBuffer.resize(0); - g.WindowsSortBuffer.reserve(g.Windows.Size); - for (int i = 0; i != g.Windows.Size; i++) - { - ImGuiWindow *window = g.Windows[i]; - if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it - continue; - AddWindowToSortedBuffer(&g.WindowsSortBuffer, window); - } - - IM_ASSERT(g.Windows.Size == g.WindowsSortBuffer.Size); // we done something wrong - g.Windows.swap(g.WindowsSortBuffer); - - // Clear Input data for next frame - g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; - memset(g.IO.InputCharacters, 0, sizeof(g.IO.InputCharacters)); - memset(g.IO.NavInputs, 0, sizeof(g.IO.NavInputs)); - - g.FrameCountEnded = g.FrameCount; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); + const float border_size = g.Style.FrameBorderSize; + if (borders && border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } } -void ImGui::Render() +void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) { - ImGuiContext &g = *GImGui; - IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() - - if (g.FrameCountEnded != g.FrameCount) - ImGui::EndFrame(); - g.FrameCountRendered = g.FrameCount; - - // Skip render altogether if alpha is 0.0 - // Note that vertex buffers have been created and are wasted, so it is best practice that you don't create windows in the first place, or consistently respond to Begin() returning false. - if (g.Style.Alpha > 0.0f) - { - // Gather windows to render - g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = g.IO.MetricsActiveWindows = 0; - g.DrawDataBuilder.Clear(); - ImGuiWindow *window_to_render_front_most = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget : NULL; - for (int n = 0; n != g.Windows.Size; n++) - { - ImGuiWindow *window = g.Windows[n]; - if (window->Active && window->HiddenFrames <= 0 && (window->Flags & (ImGuiWindowFlags_ChildWindow)) == 0 && window != window_to_render_front_most) - AddWindowToDrawDataSelectLayer(window); - } - if (window_to_render_front_most && window_to_render_front_most->Active && window_to_render_front_most->HiddenFrames <= 0) // NavWindowingTarget is always temporarily displayed as the front-most window - AddWindowToDrawDataSelectLayer(window_to_render_front_most); - g.DrawDataBuilder.FlattenIntoSingleLayer(); - - // Draw software mouse cursor if requested - ImVec2 offset, size, uv[4]; - if (g.IO.MouseDrawCursor && g.IO.Fonts->GetMouseCursorTexData(g.MouseCursor, &offset, &size, &uv[0], &uv[2])) - { - const ImVec2 pos = g.IO.MousePos - offset; - const ImTextureID tex_id = g.IO.Fonts->TexID; - const float sc = g.Style.MouseCursorScale; - g.OverlayDrawList.PushTextureID(tex_id); - g.OverlayDrawList.AddImage(tex_id, pos + ImVec2(1, 0) * sc, pos + ImVec2(1, 0) * sc + size * sc, uv[2], uv[3], IM_COL32(0, 0, 0, 48)); // Shadow - g.OverlayDrawList.AddImage(tex_id, pos + ImVec2(2, 0) * sc, pos + ImVec2(2, 0) * sc + size * sc, uv[2], uv[3], IM_COL32(0, 0, 0, 48)); // Shadow - g.OverlayDrawList.AddImage(tex_id, pos, pos + size * sc, uv[2], uv[3], IM_COL32(0, 0, 0, 255)); // Black border - g.OverlayDrawList.AddImage(tex_id, pos, pos + size * sc, uv[0], uv[1], IM_COL32(255, 255, 255, 255)); // White fill - g.OverlayDrawList.PopTextureID(); - } - if (!g.OverlayDrawList.VtxBuffer.empty()) - AddDrawListToDrawData(&g.DrawDataBuilder.Layers[0], &g.OverlayDrawList); - - // Setup ImDrawData structure for end-user - SetupDrawData(&g.DrawDataBuilder.Layers[0], &g.DrawData); - g.IO.MetricsRenderVertices = g.DrawData.TotalVtxCount; - g.IO.MetricsRenderIndices = g.DrawData.TotalIdxCount; - - // Render. If user hasn't set a callback then they may retrieve the draw data via GetDrawData() -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - if (g.DrawData.CmdListsCount > 0 && g.IO.RenderDrawListsFn != NULL) - g.IO.RenderDrawListsFn(&g.DrawData); -#endif - } + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const float border_size = g.Style.FrameBorderSize; + if (border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } } -const char *ImGui::FindRenderedTextEnd(const char *text, const char *text_end) +void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags) { - const char *text_display_end = text; - if (!text_end) - text_end = (const char *) -1; + ImGuiContext& g = *GImGui; + if (id != g.NavId) + return; + if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) + return; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.NavHideHighlightOneFrame) + return; - while (text_display_end < text_end && *text_display_end != '\0' && (text_display_end[0] != '#' || text_display_end[1] != '#')) - text_display_end++; - return text_display_end; + float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + ImRect display_rect = bb; + display_rect.ClipWith(window->ClipRect); + const float thickness = 2.0f; + if (flags & ImGuiNavHighlightFlags_Compact) + { + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, thickness); + } + else + { + const float distance = 3.0f + thickness * 0.5f; + display_rect.Expand(ImVec2(distance, distance)); + bool fully_visible = window->ClipRect.Contains(display_rect); + if (!fully_visible) + window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, 0, thickness); + if (!fully_visible) + window->DrawList->PopClipRect(); + } } -// Pass text data straight to log (without being displayed) -void ImGui::LogText(const char *fmt, ...) +void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) { - ImGuiContext &g = *GImGui; - if (!g.LogEnabled) - return; - - va_list args; - va_start(args, fmt); - if (g.LogFile) - { - vfprintf(g.LogFile, fmt, args); - } - else - { - g.LogClipboard->appendfv(fmt, args); - } - va_end(args); + ImGuiContext& g = *GImGui; + IM_ASSERT(mouse_cursor > ImGuiMouseCursor_None && mouse_cursor < ImGuiMouseCursor_COUNT); + ImFontAtlas* font_atlas = g.DrawListSharedData.Font->ContainerAtlas; + for (ImGuiViewportP* viewport : g.Viewports) + { + // We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor. + ImVec2 offset, size, uv[4]; + if (!font_atlas->GetMouseCursorTexData(mouse_cursor, &offset, &size, &uv[0], &uv[2])) + continue; + const ImVec2 pos = base_pos - offset; + const float scale = base_scale; + if (!viewport->GetMainRect().Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale))) + continue; + ImDrawList* draw_list = GetForegroundDrawList(viewport); + ImTextureID tex_id = font_atlas->TexID; + draw_list->PushTextureID(tex_id); + draw_list->AddImage(tex_id, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[2], uv[3], col_border); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[0], uv[1], col_fill); + draw_list->PopTextureID(); + } } -// Internal version that takes a position to decide on newline placement and pad items according to their depth. -// We split text into individual lines to add current tree level padding -static void LogRenderedText(const ImVec2 *ref_pos, const char *text, const char *text_end = NULL) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - if (!text_end) - text_end = ImGui::FindRenderedTextEnd(text, text_end); - - const bool log_new_line = ref_pos && (ref_pos->y > window->DC.LogLinePosY + 1); - if (ref_pos) - window->DC.LogLinePosY = ref_pos->y; - - const char *text_remaining = text; - if (g.LogStartDepth > window->DC.TreeDepth) // Re-adjust padding if we have popped out of our starting depth - g.LogStartDepth = window->DC.TreeDepth; - const int tree_depth = (window->DC.TreeDepth - g.LogStartDepth); - for (;;) - { - // Split the string. Each new line (after a '\n') is followed by spacing corresponding to the current depth of our log entry. - const char *line_end = text_remaining; - while (line_end < text_end) - if (*line_end == '\n') - break; - else - line_end++; - if (line_end >= text_end) - line_end = NULL; - - const bool is_first_line = (text == text_remaining); - bool is_last_line = false; - if (line_end == NULL) - { - is_last_line = true; - line_end = text_end; - } - if (line_end != NULL && !(is_last_line && (line_end - text_remaining) == 0)) - { - const int char_count = (int) (line_end - text_remaining); - if (log_new_line || !is_first_line) - ImGui::LogText(IM_NEWLINE "%*s%.*s", tree_depth * 4, "", char_count, text_remaining); - else - ImGui::LogText(" %.*s", char_count, text_remaining); - } - - if (is_last_line) - break; - text_remaining = line_end + 1; - } -} +//----------------------------------------------------------------------------- +// [SECTION] INITIALIZATION, SHUTDOWN +//----------------------------------------------------------------------------- -// Internal ImGui functions to render text -// RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText() -void ImGui::RenderText(ImVec2 pos, const char *text, const char *text_end, bool hide_text_after_hash) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - // Hide anything after a '##' string - const char *text_display_end; - if (hide_text_after_hash) - { - text_display_end = FindRenderedTextEnd(text, text_end); - } - else - { - if (!text_end) - text_end = text + strlen(text); // FIXME-OPT - text_display_end = text_end; - } - - const int text_len = (int) (text_display_end - text); - if (text_len > 0) - { - window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end); - if (g.LogEnabled) - LogRenderedText(&pos, text, text_display_end); - } -} - -void ImGui::RenderTextWrapped(ImVec2 pos, const char *text, const char *text_end, float wrap_width) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - if (!text_end) - text_end = text + strlen(text); // FIXME-OPT - - const int text_len = (int) (text_end - text); - if (text_len > 0) - { - window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_end, wrap_width); - if (g.LogEnabled) - LogRenderedText(&pos, text, text_end); - } +// Internal state access - if you want to share Dear ImGui state between modules (e.g. DLL) or allocate it yourself +// Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module +ImGuiContext* ImGui::GetCurrentContext() +{ + return GImGui; } -// Default clip_rect uses (pos_min,pos_max) -// Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) -void ImGui::RenderTextClipped(const ImVec2 &pos_min, const ImVec2 &pos_max, const char *text, const char *text_end, const ImVec2 *text_size_if_known, const ImVec2 &align, const ImRect *clip_rect) -{ - // Hide anything after a '##' string - const char *text_display_end = FindRenderedTextEnd(text, text_end); - const int text_len = (int) (text_display_end - text); - if (text_len == 0) - return; - - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - // Perform CPU side clipping for single clipped element to avoid using scissor state - ImVec2 pos = pos_min; - const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f); - - const ImVec2 *clip_min = clip_rect ? &clip_rect->Min : &pos_min; - const ImVec2 *clip_max = clip_rect ? &clip_rect->Max : &pos_max; - bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); - if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min - need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); - - // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. - if (align.x > 0.0f) - pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x); - if (align.y > 0.0f) - pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y); - - // Render - if (need_clipping) - { - ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); - window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); - } - else - { - window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); - } - if (g.LogEnabled) - LogRenderedText(&pos, text, text_display_end); +void ImGui::SetCurrentContext(ImGuiContext* ctx) +{ +#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. +#else + GImGui = ctx; +#endif } -// Render a rectangle shaped with optional rounding and borders -void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border, float rounding) +void ImGui::SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); - const float border_size = g.Style.FrameBorderSize; - if (border && border_size > 0.0f) - { - window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); - window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); - } + GImAllocatorAllocFunc = alloc_func; + GImAllocatorFreeFunc = free_func; + GImAllocatorUserData = user_data; } -void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) +// This is provided to facilitate copying allocators from one static/DLL boundary to another (e.g. retrieve default allocator of your executable address space) +void ImGui::GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - const float border_size = g.Style.FrameBorderSize; - if (border_size > 0.0f) - { - window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); - window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); - } -} - -// Render a triangle to denote expanded/collapsed state -void ImGui::RenderTriangle(ImVec2 p_min, ImGuiDir dir, float scale) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - const float h = g.FontSize * 1.00f; - float r = h * 0.40f * scale; - ImVec2 center = p_min + ImVec2(h * 0.50f, h * 0.50f * scale); - - ImVec2 a, b, c; - switch (dir) - { - case ImGuiDir_Up: - case ImGuiDir_Down: - if (dir == ImGuiDir_Up) - r = -r; - center.y -= r * 0.25f; - a = ImVec2(0, 1) * r; - b = ImVec2(-0.866f, -0.5f) * r; - c = ImVec2(+0.866f, -0.5f) * r; - break; - case ImGuiDir_Left: - case ImGuiDir_Right: - if (dir == ImGuiDir_Left) - r = -r; - center.x -= r * 0.25f; - a = ImVec2(1, 0) * r; - b = ImVec2(-0.500f, +0.866f) * r; - c = ImVec2(-0.500f, -0.866f) * r; - break; - case ImGuiDir_None: - case ImGuiDir_Count_: - IM_ASSERT(0); - break; - } - - window->DrawList->AddTriangleFilled(center + a, center + b, center + c, GetColorU32(ImGuiCol_Text)); -} - -void ImGui::RenderBullet(ImVec2 pos) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - window->DrawList->AddCircleFilled(pos, GImGui->FontSize * 0.20f, GetColorU32(ImGuiCol_Text), 8); -} - -void ImGui::RenderCheckMark(ImVec2 pos, ImU32 col, float sz) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - float thickness = ImMax(sz / 5.0f, 1.0f); - sz -= thickness * 0.5f; - pos += ImVec2(thickness * 0.25f, thickness * 0.25f); - - float third = sz / 3.0f; - float bx = pos.x + third; - float by = pos.y + sz - third * 0.5f; - window->DrawList->PathLineTo(ImVec2(bx - third, by - third)); - window->DrawList->PathLineTo(ImVec2(bx, by)); - window->DrawList->PathLineTo(ImVec2(bx + third * 2, by - third * 2)); - window->DrawList->PathStroke(col, false, thickness); -} - -void ImGui::RenderNavHighlight(const ImRect &bb, ImGuiID id, ImGuiNavHighlightFlags flags) -{ - ImGuiContext &g = *GImGui; - if (id != g.NavId) - return; - if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) - return; - ImGuiWindow *window = ImGui::GetCurrentWindow(); - if (window->DC.NavHideHighlightOneFrame) - return; - - float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; - ImRect display_rect = bb; - display_rect.ClipWith(window->ClipRect); - if (flags & ImGuiNavHighlightFlags_TypeDefault) - { - const float THICKNESS = 2.0f; - const float DISTANCE = 3.0f + THICKNESS * 0.5f; - display_rect.Expand(ImVec2(DISTANCE, DISTANCE)); - bool fully_visible = window->ClipRect.Contains(display_rect); - if (!fully_visible) - window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); - window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), display_rect.Max - ImVec2(THICKNESS * 0.5f, THICKNESS * 0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, ImDrawCornerFlags_All, THICKNESS); - if (!fully_visible) - window->DrawList->PopClipRect(); - } - if (flags & ImGuiNavHighlightFlags_TypeThin) - { - window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, ~0, 1.0f); - } + *p_alloc_func = GImAllocatorAllocFunc; + *p_free_func = GImAllocatorFreeFunc; + *p_user_data = GImAllocatorUserData; } -// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. -// CalcTextSize("") should return ImVec2(0.0f, GImGui->FontSize) -ImVec2 ImGui::CalcTextSize(const char *text, const char *text_end, bool hide_text_after_double_hash, float wrap_width) -{ - ImGuiContext &g = *GImGui; - - const char *text_display_end; - if (hide_text_after_double_hash) - text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string - else - text_display_end = text_end; - - ImFont *font = g.Font; - const float font_size = g.FontSize; - if (text == text_display_end) - return ImVec2(0.0f, font_size); - ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); - - // Cancel out character spacing for the last character of a line (it is baked into glyph->AdvanceX field) - const float font_scale = font_size / font->FontSize; - const float character_spacing_x = 1.0f * font_scale; - if (text_size.x > 0.0f) - text_size.x -= character_spacing_x; - text_size.x = (float) (int) (text_size.x + 0.95f); - - return text_size; -} - -// Helper to calculate coarse clipping of large list of evenly sized items. -// NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. -// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX -void ImGui::CalcListClipping(int items_count, float items_height, int *out_items_display_start, int *out_items_display_end) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (g.LogEnabled) - { - // If logging is active, do not perform any clipping - *out_items_display_start = 0; - *out_items_display_end = items_count; - return; - } - if (window->SkipItems) - { - *out_items_display_start = *out_items_display_end = 0; - return; - } - - const ImVec2 pos = window->DC.CursorPos; - int start = (int) ((window->ClipRect.Min.y - pos.y) / items_height); - int end = (int) ((window->ClipRect.Max.y - pos.y) / items_height); - if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Up) // When performing a navigation request, ensure we have one item extra in the direction we are moving to - start--; - if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) - end++; - - start = ImClamp(start, 0, items_count); - end = ImClamp(end + 1, start, items_count); - *out_items_display_start = start; - *out_items_display_end = end; +ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +{ + ImGuiContext* prev_ctx = GetCurrentContext(); + ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + SetCurrentContext(ctx); + Initialize(); + if (prev_ctx != NULL) + SetCurrentContext(prev_ctx); // Restore previous context if any, else keep new one. + return ctx; } -// Find window given position, search front-to-back -// FIXME: Note that we have a lag here because WindowRectClipped is updated in Begin() so windows moved by user via SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is called, aka before the next Begin(). Moving window thankfully isn't affected. -static ImGuiWindow *FindHoveredWindow() -{ - ImGuiContext &g = *GImGui; - for (int i = g.Windows.Size - 1; i >= 0; i--) - { - ImGuiWindow *window = g.Windows[i]; - if (!window->Active) - continue; - if (window->Flags & ImGuiWindowFlags_NoInputs) - continue; - - // Using the clipped AABB, a child window will typically be clipped by its parent (not always) - ImRect bb(window->WindowRectClipped.Min - g.Style.TouchExtraPadding, window->WindowRectClipped.Max + g.Style.TouchExtraPadding); - if (bb.Contains(g.IO.MousePos)) - return window; - } - return NULL; +void ImGui::DestroyContext(ImGuiContext* ctx) +{ + ImGuiContext* prev_ctx = GetCurrentContext(); + if (ctx == NULL) //-V1051 + ctx = prev_ctx; + SetCurrentContext(ctx); + Shutdown(); + SetCurrentContext((prev_ctx != ctx) ? prev_ctx : NULL); + IM_DELETE(ctx); } -// Test if mouse cursor is hovering given rectangle -// NB- Rectangle is clipped by our current clip setting -// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) -bool ImGui::IsMouseHoveringRect(const ImVec2 &r_min, const ImVec2 &r_max, bool clip) +// IMPORTANT: ###xxx suffixes must be same in ALL languages to allow for automation. +static const ImGuiLocEntry GLocalizationEntriesEnUS[] = +{ + { ImGuiLocKey_VersionStr, "Dear ImGui " IMGUI_VERSION " (" IM_STRINGIFY(IMGUI_VERSION_NUM) ")" }, + { ImGuiLocKey_TableSizeOne, "Size column to fit###SizeOne" }, + { ImGuiLocKey_TableSizeAllFit, "Size all columns to fit###SizeAll" }, + { ImGuiLocKey_TableSizeAllDefault, "Size all columns to default###SizeAll" }, + { ImGuiLocKey_TableResetOrder, "Reset order###ResetOrder" }, + { ImGuiLocKey_WindowingMainMenuBar, "(Main menu bar)" }, + { ImGuiLocKey_WindowingPopup, "(Popup)" }, + { ImGuiLocKey_WindowingUntitled, "(Untitled)" }, + { ImGuiLocKey_CopyLink, "Copy Link###CopyLink" }, +}; + +void ImGui::Initialize() { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; + ImGuiContext& g = *GImGui; + IM_ASSERT(!g.Initialized && !g.SettingsLoaded); - // Clip - ImRect rect_clipped(r_min, r_max); - if (clip) - rect_clipped.ClipWith(window->ClipRect); + // Add .ini handle for ImGuiWindow and ImGuiTable types + { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Window"; + ini_handler.TypeHash = ImHashStr("Window"); + ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = WindowSettingsHandler_WriteAll; + AddSettingsHandler(&ini_handler); + } + TableSettingsAddSettingsHandler(); + + // Setup default localization table + LocalizeRegisterEntries(GLocalizationEntriesEnUS, IM_ARRAYSIZE(GLocalizationEntriesEnUS)); + + // Setup default ImGuiPlatformIO clipboard/IME handlers. + g.PlatformIO.Platform_GetClipboardTextFn = Platform_GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + g.PlatformIO.Platform_SetClipboardTextFn = Platform_SetClipboardTextFn_DefaultImpl; + g.PlatformIO.Platform_OpenInShellFn = Platform_OpenInShellFn_DefaultImpl; + g.PlatformIO.Platform_SetImeDataFn = Platform_SetImeDataFn_DefaultImpl; + + // Create default viewport + ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = IMGUI_VIEWPORT_DEFAULT_ID; + g.Viewports.push_back(viewport); + g.TempBuffer.resize(1024 * 3 + 1, 0); + + // Build KeysMayBeCharInput[] lookup table (1 bool per named key) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + if ((key >= ImGuiKey_0 && key <= ImGuiKey_9) || (key >= ImGuiKey_A && key <= ImGuiKey_Z) || (key >= ImGuiKey_Keypad0 && key <= ImGuiKey_Keypad9) + || key == ImGuiKey_Tab || key == ImGuiKey_Space || key == ImGuiKey_Apostrophe || key == ImGuiKey_Comma || key == ImGuiKey_Minus || key == ImGuiKey_Period + || key == ImGuiKey_Slash || key == ImGuiKey_Semicolon || key == ImGuiKey_Equal || key == ImGuiKey_LeftBracket || key == ImGuiKey_RightBracket || key == ImGuiKey_GraveAccent + || key == ImGuiKey_KeypadDecimal || key == ImGuiKey_KeypadDivide || key == ImGuiKey_KeypadMultiply || key == ImGuiKey_KeypadSubtract || key == ImGuiKey_KeypadAdd || key == ImGuiKey_KeypadEqual) + g.KeysMayBeCharInput.SetBit(key); + +#ifdef IMGUI_HAS_DOCK +#endif - // Expand for touch input - const ImRect rect_for_touch(rect_clipped.Min - g.Style.TouchExtraPadding, rect_clipped.Max + g.Style.TouchExtraPadding); - return rect_for_touch.Contains(g.IO.MousePos); + g.Initialized = true; } -static bool IsKeyPressedMap(ImGuiKey key, bool repeat) +// This function is merely here to free heap allocations. +void ImGui::Shutdown() { - const int key_index = GImGui->IO.KeyMap[key]; - return (key_index >= 0) ? ImGui::IsKeyPressed(key_index, repeat) : false; + ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR(g.IO.BackendPlatformUserData == NULL, "Forgot to shutdown Platform backend?"); + IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == NULL, "Forgot to shutdown Renderer backend?"); + + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) + if (g.IO.Fonts && g.FontAtlasOwnedByContext) + { + g.IO.Fonts->Locked = false; + IM_DELETE(g.IO.Fonts); + } + g.IO.Fonts = NULL; + g.DrawListSharedData.TempBuffer.clear(); + + // Cleanup of other data are conditional on actually having initialized Dear ImGui. + if (!g.Initialized) + return; + + // Save settings (unless we haven't attempted to load them: CreateContext/DestroyContext without a call to NewFrame shouldn't save an empty file) + if (g.SettingsLoaded && g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + + CallContextHooks(&g, ImGuiContextHookType_Shutdown); + + // Clear everything else + g.Windows.clear_delete(); + g.WindowsFocusOrder.clear(); + g.WindowsTempSortBuffer.clear(); + g.CurrentWindow = NULL; + g.CurrentWindowStack.clear(); + g.WindowsById.Clear(); + g.NavWindow = NULL; + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + g.ActiveIdWindow = g.ActiveIdPreviousFrameWindow = NULL; + g.MovingWindow = NULL; + + g.KeysRoutingTable.Clear(); + + g.ColorStack.clear(); + g.StyleVarStack.clear(); + g.FontStack.clear(); + g.OpenPopupStack.clear(); + g.BeginPopupStack.clear(); + g.TreeNodeStack.clear(); + + g.Viewports.clear_delete(); + + g.TabBars.Clear(); + g.CurrentTabBarStack.clear(); + g.ShrinkWidthBuffer.clear(); + + g.ClipperTempData.clear_destruct(); + + g.Tables.Clear(); + g.TablesTempData.clear_destruct(); + g.DrawChannelsTempMergeBuffer.clear(); + + g.MultiSelectStorage.Clear(); + g.MultiSelectTempData.clear_destruct(); + + g.ClipboardHandlerData.clear(); + g.MenusIdSubmittedThisFrame.clear(); + g.InputTextState.ClearFreeMemory(); + g.InputTextDeactivatedState.ClearFreeMemory(); + + g.SettingsWindows.clear(); + g.SettingsHandlers.clear(); + + if (g.LogFile) + { +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + if (g.LogFile != stdout) +#endif + ImFileClose(g.LogFile); + g.LogFile = NULL; + } + g.LogBuffer.clear(); + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + + g.Initialized = false; } -int ImGui::GetKeyIndex(ImGuiKey imgui_key) +// No specific ordering/dependency support, will see as needed +ImGuiID ImGui::AddContextHook(ImGuiContext* ctx, const ImGuiContextHook* hook) { - IM_ASSERT(imgui_key >= 0 && imgui_key < ImGuiKey_COUNT); - return GImGui->IO.KeyMap[imgui_key]; + ImGuiContext& g = *ctx; + IM_ASSERT(hook->Callback != NULL && hook->HookId == 0 && hook->Type != ImGuiContextHookType_PendingRemoval_); + g.Hooks.push_back(*hook); + g.Hooks.back().HookId = ++g.HookIdNext; + return g.HookIdNext; } -// Note that imgui doesn't know the semantic of each entry of io.KeyDown[]. Use your own indices/enums according to how your back-end/engine stored them into KeyDown[]! -bool ImGui::IsKeyDown(int user_key_index) +// Deferred removal, avoiding issue with changing vector while iterating it +void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) { - if (user_key_index < 0) - return false; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(GImGui->IO.KeysDown)); - return GImGui->IO.KeysDown[user_key_index]; + ImGuiContext& g = *ctx; + IM_ASSERT(hook_id != 0); + for (ImGuiContextHook& hook : g.Hooks) + if (hook.HookId == hook_id) + hook.Type = ImGuiContextHookType_PendingRemoval_; } -int ImGui::CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate) +// Call context hooks (used by e.g. test engine) +// We assume a small number of hooks so all stored in same array +void ImGui::CallContextHooks(ImGuiContext* ctx, ImGuiContextHookType hook_type) { - if (t == 0.0f) - return 1; - if (t <= repeat_delay || repeat_rate <= 0.0f) - return 0; - const int count = (int) ((t - repeat_delay) / repeat_rate) - (int) ((t_prev - repeat_delay) / repeat_rate); - return (count > 0) ? count : 0; + ImGuiContext& g = *ctx; + for (ImGuiContextHook& hook : g.Hooks) + if (hook.Type == hook_type) + hook.Callback(&g, &hook); +} + + +//----------------------------------------------------------------------------- +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +//----------------------------------------------------------------------------- + +// ImGuiWindow is mostly a dumb struct. It merely has a constructor and a few helper methods +ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NULL) +{ + memset(this, 0, sizeof(*this)); + Ctx = ctx; + Name = ImStrdup(name); + NameBufLen = (int)strlen(name) + 1; + ID = ImHashStr(name); + IDStack.push_back(ID); + MoveId = GetID("#MOVE"); + ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); + AutoFitFramesX = AutoFitFramesY = -1; + AutoPosLastDirection = ImGuiDir_None; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = 0; + SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); + LastFrameActive = -1; + LastTimeActive = -1.0f; + FontWindowScale = 1.0f; + SettingsOffset = -1; + DrawList = &DrawListInst; + DrawList->_Data = &Ctx->DrawListSharedData; + DrawList->_OwnerName = Name; + NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX); } -int ImGui::GetKeyPressedAmount(int key_index, float repeat_delay, float repeat_rate) +ImGuiWindow::~ImGuiWindow() { - ImGuiContext &g = *GImGui; - if (key_index < 0) - return false; - IM_ASSERT(key_index >= 0 && key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - const float t = g.IO.KeysDownDuration[key_index]; - return CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, repeat_delay, repeat_rate); + IM_ASSERT(DrawList == &DrawListInst); + IM_DELETE(Name); + ColumnsStorage.clear_destruct(); } -bool ImGui::IsKeyPressed(int user_key_index, bool repeat) +static void SetCurrentWindow(ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - if (user_key_index < 0) - return false; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - const float t = g.IO.KeysDownDuration[user_key_index]; - if (t == 0.0f) - return true; - if (repeat && t > g.IO.KeyRepeatDelay) - return GetKeyPressedAmount(user_key_index, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; - return false; + ImGuiContext& g = *GImGui; + g.CurrentWindow = window; + g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL; + g.CurrentDpiScale = 1.0f; // FIXME-DPI: WIP this is modified in docking + if (window) + { + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.FontSize / g.Font->FontSize; + ImGui::NavUpdateCurrentWindowIsScrollPushableX(); + } } -bool ImGui::IsKeyReleased(int user_key_index) +void ImGui::GcCompactTransientMiscBuffers() { - ImGuiContext &g = *GImGui; - if (user_key_index < 0) - return false; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - return g.IO.KeysDownDurationPrev[user_key_index] >= 0.0f && !g.IO.KeysDown[user_key_index]; + ImGuiContext& g = *GImGui; + g.ItemFlagsStack.clear(); + g.GroupStack.clear(); + g.MultiSelectTempDataStacked = 0; + g.MultiSelectTempData.clear_destruct(); + TableGcCompactSettings(); } -bool ImGui::IsMouseDown(int button) +// Free up/compact internal window buffers, we can use this when a window becomes unused. +// Not freed: +// - ImGuiWindow, ImGuiWindowSettings, Name, StateStorage, ColumnsStorage (may hold useful data) +// This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost. +void ImGui::GcCompactTransientWindowBuffers(ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDown[button]; + window->MemoryCompacted = true; + window->MemoryDrawListIdxCapacity = window->DrawList->IdxBuffer.Capacity; + window->MemoryDrawListVtxCapacity = window->DrawList->VtxBuffer.Capacity; + window->IDStack.clear(); + window->DrawList->_ClearFreeMemory(); + window->DC.ChildWindows.clear(); + window->DC.ItemWidthStack.clear(); + window->DC.TextWrapPosStack.clear(); } -bool ImGui::IsAnyMouseDown() +void ImGui::GcAwakeTransientWindowBuffers(ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) - if (g.IO.MouseDown[n]) - return true; - return false; + // We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening. + // The other buffers tends to amortize much faster. + window->MemoryCompacted = false; + window->DrawList->IdxBuffer.reserve(window->MemoryDrawListIdxCapacity); + window->DrawList->VtxBuffer.reserve(window->MemoryDrawListVtxCapacity); + window->MemoryDrawListIdxCapacity = window->MemoryDrawListVtxCapacity = 0; } -bool ImGui::IsMouseClicked(int button, bool repeat) +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - const float t = g.IO.MouseDownDuration[button]; - if (t == 0.0f) - return true; + ImGuiContext& g = *GImGui; + + // Clear previous active id + if (g.ActiveId != 0) + { + // While most behaved code would make an effort to not steal active id during window move/drag operations, + // we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch + // may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that. + if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n"); + g.MovingWindow = NULL; + } + + // This could be written in a more general way (e.g associate a hook to ActiveId), + // but since this is currently quite an exception we'll leave it as is. + // One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveId() + if (g.InputTextState.ID == g.ActiveId) + InputTextDeactivateHook(g.ActiveId); + } - if (repeat && t > g.IO.KeyRepeatDelay) - { - float delay = g.IO.KeyRepeatDelay, rate = g.IO.KeyRepeatRate; - if ((fmodf(t - delay, rate) > rate * 0.5f) != (fmodf(t - delay - g.IO.DeltaTime, rate) > rate * 0.5f)) - return true; - } + // Set active id + g.ActiveIdIsJustActivated = (g.ActiveId != id); + if (g.ActiveIdIsJustActivated) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() old:0x%08X (window \"%s\") -> new:0x%08X (window \"%s\")\n", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "", id, window ? window->Name : ""); + g.ActiveIdTimer = 0.0f; + g.ActiveIdHasBeenPressedBefore = false; + g.ActiveIdHasBeenEditedBefore = false; + g.ActiveIdMouseButton = -1; + if (id != 0) + { + g.LastActiveId = id; + g.LastActiveIdTimer = 0.0f; + } + } + g.ActiveId = id; + g.ActiveIdAllowOverlap = false; + g.ActiveIdNoClearOnFocusLoss = false; + g.ActiveIdWindow = window; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdFromShortcut = false; + if (id) + { + g.ActiveIdIsAlive = id; + g.ActiveIdSource = (g.NavActivateId == id || g.NavJustMovedToId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + IM_ASSERT(g.ActiveIdSource != ImGuiInputSource_None); + } - return false; + // Clear declaration of inputs claimed by the widget + // (Please note that this is WIP and not all keys/inputs are thoroughly declared by all widgets yet) + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; } -bool ImGui::IsMouseReleased(int button) +void ImGui::ClearActiveID() { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseReleased[button]; + SetActiveID(0, NULL); // g.ActiveId = 0; } -bool ImGui::IsMouseDoubleClicked(int button) +void ImGui::SetHoveredID(ImGuiID id) { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDoubleClicked[button]; + ImGuiContext& g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + if (id != 0 && g.HoveredIdPreviousFrame != id) + g.HoveredIdTimer = g.HoveredIdNotActiveTimer = 0.0f; } -bool ImGui::IsMouseDragging(int button, float lock_threshold) +ImGuiID ImGui::GetHoveredID() { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - if (!g.IO.MouseDown[button]) - return false; - if (lock_threshold < 0.0f) - lock_threshold = g.IO.MouseDragThreshold; - return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; + ImGuiContext& g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; } -ImVec2 ImGui::GetMousePos() +void ImGui::MarkItemEdited(ImGuiID id) { - return GImGui->IO.MousePos; + // This marking is solely to be able to provide info for IsItemDeactivatedAfterEdit(). + // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. + ImGuiContext& g = *GImGui; + if (g.LockMarkEdited > 0) + return; + if (g.ActiveId == id || g.ActiveId == 0) + { + g.ActiveIdHasBeenEditedThisFrame = true; + g.ActiveIdHasBeenEditedBefore = true; + } + + // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) + // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); + + //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; } -// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! -ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() +bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) { - ImGuiContext &g = *GImGui; - if (g.CurrentPopupStack.Size > 0) - return g.OpenPopupStack[g.CurrentPopupStack.Size - 1].OpenMousePos; - return g.IO.MousePos; + // An active popup disable hovering on other windows (apart from its own children) + // FIXME-OPT: This could be cached/stored within the window. + ImGuiContext& g = *GImGui; + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) + if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The 'else' is important because Modal windows are also Popups. + bool want_inhibit = false; + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + want_inhibit = true; + else if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + want_inhibit = true; + + // Inhibit hover unless the window is within the stack of our modal/popup + if (want_inhibit) + if (!IsWindowWithinBeginStackOf(window->RootWindow, focused_root_window)) + return false; + } + return true; } -// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position -bool ImGui::IsMousePosValid(const ImVec2 *mouse_pos) +static inline float CalcDelayFromHoveredFlags(ImGuiHoveredFlags flags) { - if (mouse_pos == NULL) - mouse_pos = &GImGui->IO.MousePos; - const float MOUSE_INVALID = -256000.0f; - return mouse_pos->x >= MOUSE_INVALID && mouse_pos->y >= MOUSE_INVALID; + ImGuiContext& g = *GImGui; + if (flags & ImGuiHoveredFlags_DelayNormal) + return g.Style.HoverDelayNormal; + if (flags & ImGuiHoveredFlags_DelayShort) + return g.Style.HoverDelayShort; + return 0.0f; } -// NB: This is only valid if IsMousePosValid(). Back-ends in theory should always keep mouse position valid when dragging even outside the client window. -ImVec2 ImGui::GetMouseDragDelta(int button, float lock_threshold) +static ImGuiHoveredFlags ApplyHoverFlagsForTooltip(ImGuiHoveredFlags user_flags, ImGuiHoveredFlags shared_flags) { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - if (lock_threshold < 0.0f) - lock_threshold = g.IO.MouseDragThreshold; - if (g.IO.MouseDown[button]) - if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) - return g.IO.MousePos - g.IO.MouseClickedPos[button]; // Assume we can only get active with left-mouse button (at the moment). - return ImVec2(0.0f, 0.0f); + // Allow instance flags to override shared flags + if (user_flags & (ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal)) + shared_flags &= ~(ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal); + return user_flags | shared_flags; } -void ImGui::ResetMouseDragDelta(int button) +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) { - ImGuiContext &g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr - g.IO.MouseClickedPos[button] = g.IO.MousePos; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT((flags & ~ImGuiHoveredFlags_AllowedMaskForIsItemHovered) == 0 && "Invalid flags for IsItemHovered()!"); + + if (g.NavDisableMouseHover && !g.NavDisableHighlight && !(flags & ImGuiHoveredFlags_NoNavOverride)) + { + if ((g.LastItemData.InFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + if (!IsItemFocused()) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipNav); + } + else + { + // Test for bounding box overlap, as updated as ItemAdd() + ImGuiItemStatusFlags status_flags = g.LastItemData.StatusFlags; + if (!(status_flags & ImGuiItemStatusFlags_HoveredRect)) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + + IM_ASSERT((flags & (ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) == 0); // Flags not supported by this function + + // Done with rectangle culling so we can perform heavier checks now + // Test if we are hovering the right window (our window could be behind another window) + // [2021/03/02] Reworked / reverted the revert, finally. Note we want e.g. BeginGroup/ItemAdd/EndGroup to work as well. (#3851) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable + // to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was + // the test that has been running for a long while. + if (g.HoveredWindow != window && (status_flags & ImGuiItemStatusFlags_HoveredWindow) == 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByWindow) == 0) + return false; + + // Test if another item is active (e.g. being dragged) + const ImGuiID id = g.LastItemData.ID; + if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0) + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (g.ActiveId != window->MoveId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal. + // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. + if (!IsWindowContentHoverable(window, flags) && !(g.LastItemData.InFlags & ImGuiItemFlags_NoWindowHoverableCheck)) + return false; + + // Test if the item is disabled + if ((g.LastItemData.InFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + // Special handling for calling after Begin() which represent the title bar or tab. + // When the window is skipped/collapsed (SkipItems==true) that last item (always ->MoveId submitted by Begin) + // will never be overwritten so we need to detect the case. + if (id == window->MoveId && window->WriteAccessed) + return false; + + // Test if using AllowOverlap and overlapped + if ((g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap) && id != 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByItem) == 0) + if (g.HoveredIdPreviousFrame != g.LastItemData.ID) + return false; + } + + // Handle hover delay + // (some ideas: https://www.nngroup.com/articles/timing-exposing-content) + const float delay = CalcDelayFromHoveredFlags(flags); + if (delay > 0.0f || (flags & ImGuiHoveredFlags_Stationary)) + { + ImGuiID hover_delay_id = (g.LastItemData.ID != 0) ? g.LastItemData.ID : window->GetIDFromPos(g.LastItemData.Rect.Min); + if ((flags & ImGuiHoveredFlags_NoSharedDelay) && (g.HoverItemDelayIdPreviousFrame != hover_delay_id)) + g.HoverItemDelayTimer = 0.0f; + g.HoverItemDelayId = hover_delay_id; + + // When changing hovered item we requires a bit of stationary delay before activating hover timer, + // but once unlocked on a given item we also moving. + //if (g.HoverDelayTimer >= delay && (g.HoverDelayTimer - g.IO.DeltaTime < delay || g.MouseStationaryTimer - g.IO.DeltaTime < g.Style.HoverStationaryDelay)) { IMGUI_DEBUG_LOG("HoverDelayTimer = %f/%f, MouseStationaryTimer = %f\n", g.HoverDelayTimer, delay, g.MouseStationaryTimer); } + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverItemUnlockedStationaryId != hover_delay_id) + return false; + + if (g.HoverItemDelayTimer < delay) + return false; + } + + return true; } -ImGuiMouseCursor ImGui::GetMouseCursor() -{ - return GImGui->MouseCursor; +// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). +// (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call) +// FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28. +// If you used this in your legacy/custom widgets code: +// - Commonly: if your ItemHoverable() call comes after an ItemAdd() call: pass 'item_flags = g.LastItemData.InFlags'. +// - Rare: otherwise you may pass 'item_flags = 0' (ImGuiItemFlags_None) unless you want to benefit from special behavior handled by ItemHoverable. +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.HoveredWindow != window) + return false; + if (!IsMouseHoveringRect(bb.Min, bb.Max)) + return false; + + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (!g.ActiveIdFromShortcut) + return false; + + // Done with rectangle culling so we can perform heavier checks now. + if (!(item_flags & ImGuiItemFlags_NoWindowHoverableCheck) && !IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) + { + g.HoveredIdIsDisabled = true; + return false; + } + + // We exceptionally allow this function to be called with id==0 to allow using it for easy high-level + // hover test in widgets code. We could also decide to split this function is two. + if (id != 0) + { + // Drag source doesn't report as hovered + if (g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) + return false; + + SetHoveredID(id); + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. + // This allows using patterns where a later submitted widget overlaps a previous one. Generally perceived as a front-to-back hit-test. + if (item_flags & ImGuiItemFlags_AllowOverlap) + { + g.HoveredIdAllowOverlap = true; + if (g.HoveredIdPreviousFrame != id) + return false; + } + + // Display shortcut (only works with mouse) + // (ImGuiItemStatusFlags_HasShortcut in LastItemData denotes we want a tooltip) + if (id == g.LastItemData.ID && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasShortcut)) + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal)) + SetTooltip("%s", GetKeyChordName(g.LastItemData.Shortcut)); + } + + // When disabled we'll return false but still set HoveredId + if (item_flags & ImGuiItemFlags_Disabled) + { + // Release active id if turning disabled + if (g.ActiveId == id && id != 0) + ClearActiveID(); + g.HoveredIdIsDisabled = true; + return false; + } + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + // [DEBUG] Item Picker tool! + // We perform the check here because reaching is path is rare (1~ time a frame), + // making the cost of this tool near-zero! We could get better call-stack and support picking non-hovered + // items if we performed the test in ItemAdd(), but that would incur a bigger runtime cost. + if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) + GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); + if (g.DebugItemPickerBreakId == id) + IM_DEBUG_BREAK(); + } +#endif + + if (g.NavDisableMouseHover) + return false; + + return true; } -void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +// FIXME: This is inlined/duplicated in ItemAdd() +// FIXME: The id != 0 path is not used by our codebase, may get rid of it? +bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id) { - GImGui->MouseCursor = cursor_type; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!bb.Overlaps(window->ClipRect)) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return true; + return false; } -void ImGui::CaptureKeyboardFromApp(bool capture) +// This is also inlined in ItemAdd() +// Note: if ImGuiItemStatusFlags_HasDisplayRect is set, user needs to set g.LastItemData.DisplayRect. +void ImGui::SetLastItemData(ImGuiID item_id, ImGuiItemFlags in_flags, ImGuiItemStatusFlags item_flags, const ImRect& item_rect) { - GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; + ImGuiContext& g = *GImGui; + g.LastItemData.ID = item_id; + g.LastItemData.InFlags = in_flags; + g.LastItemData.StatusFlags = item_flags; + g.LastItemData.Rect = g.LastItemData.NavRect = item_rect; } -void ImGui::CaptureMouseFromApp(bool capture) +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) { - GImGui->WantCaptureMouseNextFrame = capture ? 1 : 0; + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (wrap_pos_x == 0.0f) + { + // We could decide to setup a default wrapping max point for auto-resizing windows, + // or have auto-wrap (with unspecified wrapping pos) behave as a ContentSize extending function? + //if (window->Hidden && (window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) + // wrap_pos_x = ImMax(window->WorkRect.Min.x + g.FontSize * 10.0f, window->WorkRect.Max.x); + //else + wrap_pos_x = window->WorkRect.Max.x; + } + else if (wrap_pos_x > 0.0f) + { + wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + } + + return ImMax(wrap_pos_x - pos.x, 1.0f); } -bool ImGui::IsItemActive() +// IM_ALLOC() == ImGui::MemAlloc() +void* ImGui::MemAlloc(size_t size) { - ImGuiContext &g = *GImGui; - if (g.ActiveId) - { - ImGuiWindow *window = g.CurrentWindow; - return g.ActiveId == window->DC.LastItemId; - } - return false; + void* ptr = (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, size); +#endif + return ptr; } -bool ImGui::IsItemFocused() +// IM_FREE() == ImGui::MemFree() +void ImGui::MemFree(void* ptr) { - ImGuiContext &g = *GImGui; - return g.NavId && !g.NavDisableHighlight && g.NavId == g.CurrentWindow->DC.LastItemId; +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ptr != NULL) + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, (size_t)-1); +#endif + return (*GImAllocatorFreeFunc)(ptr, GImAllocatorUserData); } -bool ImGui::IsItemClicked(int mouse_button) +// We record the number of allocation in recent frames, as a way to audit/sanitize our guiding principles of "no allocations on idle/repeating frames" +void ImGui::DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size) { - return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_Default); + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + IM_UNUSED(ptr); + if (entry->FrameCount != frame_count) + { + info->LastEntriesIdx = (info->LastEntriesIdx + 1) % IM_ARRAYSIZE(info->LastEntriesBuf); + entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + entry->FrameCount = frame_count; + entry->AllocCount = entry->FreeCount = 0; + } + if (size != (size_t)-1) + { + entry->AllocCount++; + info->TotalAllocCount++; + //printf("[%05d] MemAlloc(%d) -> 0x%p\n", frame_count, size, ptr); + } + else + { + entry->FreeCount++; + info->TotalFreeCount++; + //printf("[%05d] MemFree(0x%p)\n", frame_count, ptr); + } } -bool ImGui::IsAnyItemHovered() +const char* ImGui::GetClipboardText() { - ImGuiContext &g = *GImGui; - return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; + ImGuiContext& g = *GImGui; + return g.PlatformIO.Platform_GetClipboardTextFn ? g.PlatformIO.Platform_GetClipboardTextFn(&g) : ""; } -bool ImGui::IsAnyItemActive() +void ImGui::SetClipboardText(const char* text) { - ImGuiContext &g = *GImGui; - return g.ActiveId != 0; + ImGuiContext& g = *GImGui; + if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) + g.PlatformIO.Platform_SetClipboardTextFn(&g, text); } -bool ImGui::IsAnyItemFocused() +const char* ImGui::GetVersion() { - ImGuiContext &g = *GImGui; - return g.NavId != 0 && !g.NavDisableHighlight; + return IMGUI_VERSION; } -bool ImGui::IsItemVisible() +ImGuiIO& ImGui::GetIO() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(window->DC.LastItemRect); + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->IO; } -// Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. -void ImGui::SetItemAllowOverlap() +ImGuiPlatformIO& ImGui::GetPlatformIO() { - ImGuiContext &g = *GImGui; - if (g.HoveredId == g.CurrentWindow->DC.LastItemId) - g.HoveredIdAllowOverlap = true; - if (g.ActiveId == g.CurrentWindow->DC.LastItemId) - g.ActiveIdAllowOverlap = true; + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext()?"); + return GImGui->PlatformIO; } -ImVec2 ImGui::GetItemRectMin() +// Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame() +ImDrawData* ImGui::GetDrawData() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Min; + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + return viewport->DrawDataP.Valid ? &viewport->DrawDataP : NULL; } -ImVec2 ImGui::GetItemRectMax() +double ImGui::GetTime() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Max; + return GImGui->Time; } -ImVec2 ImGui::GetItemRectSize() +int ImGui::GetFrameCount() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.LastItemRect.GetSize(); + return GImGui->FrameCount; } -static ImRect GetViewportRect() +static ImDrawList* GetViewportBgFgDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) { - ImGuiContext &g = *GImGui; - if (g.IO.DisplayVisibleMin.x != g.IO.DisplayVisibleMax.x && g.IO.DisplayVisibleMin.y != g.IO.DisplayVisibleMax.y) - return ImRect(g.IO.DisplayVisibleMin, g.IO.DisplayVisibleMax); - return ImRect(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); + // Create the draw list on demand, because they are not frequently used for all viewports + ImGuiContext& g = *GImGui; + IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->BgFgDrawLists)); + ImDrawList* draw_list = viewport->BgFgDrawLists[drawlist_no]; + if (draw_list == NULL) + { + draw_list = IM_NEW(ImDrawList)(&g.DrawListSharedData); + draw_list->_OwnerName = drawlist_name; + viewport->BgFgDrawLists[drawlist_no] = draw_list; + } + + // Our ImDrawList system requires that there is always a command + if (viewport->BgFgDrawListsLastFrame[drawlist_no] != g.FrameCount) + { + draw_list->_ResetForNewFrame(); + draw_list->PushTextureID(g.IO.Fonts->TexID); + draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false); + viewport->BgFgDrawListsLastFrame[drawlist_no] = g.FrameCount; + } + return draw_list; } -// Not exposed publicly as BeginTooltip() because bool parameters are evil. Let's see if other needs arise first. -void ImGui::BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip) +ImDrawList* ImGui::GetBackgroundDrawList(ImGuiViewport* viewport) { - ImGuiContext &g = *GImGui; - char window_name[16]; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); - if (override_previous_tooltip) - if (ImGuiWindow *window = FindWindowByName(window_name)) - if (window->Active) - { - // Hide previous tooltips. We can't easily "reset" the content of a window so we create a new one. - window->HiddenFrames = 1; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); - } - ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav; - Begin(window_name, NULL, flags | extra_flags); + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 0, "##Background"); } -void ImGui::SetTooltipV(const char *fmt, va_list args) +ImDrawList* ImGui::GetBackgroundDrawList() { - BeginTooltipEx(0, true); - TextV(fmt, args); - EndTooltip(); + ImGuiContext& g = *GImGui; + return GetBackgroundDrawList(g.Viewports[0]); } -void ImGui::SetTooltip(const char *fmt, ...) +ImDrawList* ImGui::GetForegroundDrawList(ImGuiViewport* viewport) { - va_list args; - va_start(args, fmt); - SetTooltipV(fmt, args); - va_end(args); + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); } -void ImGui::BeginTooltip() +ImDrawList* ImGui::GetForegroundDrawList() { - BeginTooltipEx(0, false); + ImGuiContext& g = *GImGui; + return GetForegroundDrawList(g.Viewports[0]); } -void ImGui::EndTooltip() +ImDrawListSharedData* ImGui::GetDrawListSharedData() { - IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls - End(); + return &GImGui->DrawListSharedData; } -// Mark popup as open (toggle toward open state). -// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. -// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). -// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) -void ImGui::OpenPopupEx(ImGuiID id) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *parent_window = g.CurrentWindow; - int current_stack_size = g.CurrentPopupStack.Size; - ImGuiPopupRef popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. - popup_ref.PopupId = id; - popup_ref.Window = NULL; - popup_ref.ParentWindow = parent_window; - popup_ref.OpenFrameCount = g.FrameCount; - popup_ref.OpenParentId = parent_window->IDStack.back(); - popup_ref.OpenMousePos = g.IO.MousePos; - popup_ref.OpenPopupPos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; - - if (g.OpenPopupStack.Size < current_stack_size + 1) - { - g.OpenPopupStack.push_back(popup_ref); - } - else - { - // Close child popups if any - g.OpenPopupStack.resize(current_stack_size + 1); - - // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui - // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing - // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. - if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) - g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; - else - g.OpenPopupStack[current_stack_size] = popup_ref; - - // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). - // This is equivalent to what ClosePopupToLevel() does. - // if (g.OpenPopupStack[current_stack_size].PopupId == id) - // FocusWindow(parent_window); - } -} - -void ImGui::OpenPopup(const char *str_id) -{ - ImGuiContext &g = *GImGui; - OpenPopupEx(g.CurrentWindow->GetID(str_id)); -} - -void ImGui::ClosePopupsOverWindow(ImGuiWindow *ref_window) -{ - ImGuiContext &g = *GImGui; - if (g.OpenPopupStack.empty()) - return; - - // When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. - // Don't close our own child popup windows. - int n = 0; - if (ref_window) - { - for (n = 0; n < g.OpenPopupStack.Size; n++) - { - ImGuiPopupRef &popup = g.OpenPopupStack[n]; - if (!popup.Window) - continue; - IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); - if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) - continue; - - // Trim the stack if popups are not direct descendant of the reference window (which is often the NavWindow) - bool has_focus = false; - for (int m = n; m < g.OpenPopupStack.Size && !has_focus; m++) - has_focus = (g.OpenPopupStack[m].Window && g.OpenPopupStack[m].Window->RootWindow == ref_window->RootWindow); - if (!has_focus) - break; - } - } - if (n < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the block below - ClosePopupToLevel(n); -} - -static ImGuiWindow *GetFrontMostModalRootWindow() -{ - ImGuiContext &g = *GImGui; - for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) - if (ImGuiWindow *popup = g.OpenPopupStack.Data[n].Window) - if (popup->Flags & ImGuiWindowFlags_Modal) - return popup; - return NULL; -} - -static void ClosePopupToLevel(int remaining) -{ - IM_ASSERT(remaining >= 0); - ImGuiContext &g = *GImGui; - ImGuiWindow *focus_window = (remaining > 0) ? g.OpenPopupStack[remaining - 1].Window : g.OpenPopupStack[0].ParentWindow; - if (g.NavLayer == 0) - focus_window = NavRestoreLastChildNavWindow(focus_window); - ImGui::FocusWindow(focus_window); - focus_window->DC.NavHideHighlightOneFrame = true; - g.OpenPopupStack.resize(remaining); -} - -void ImGui::ClosePopup(ImGuiID id) -{ - if (!IsPopupOpen(id)) - return; - ImGuiContext &g = *GImGui; - ClosePopupToLevel(g.OpenPopupStack.Size - 1); +void ImGui::StartMouseMovingWindow(ImGuiWindow* window) +{ + // Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows. + // We _also_ call this when clicking in a window empty space when io.ConfigWindowsMoveFromTitleBarOnly is set, but clear g.MovingWindow afterward. + // This is because we want ActiveId to be set even when the window is not permitted to move. + ImGuiContext& g = *GImGui; + FocusWindow(window); + SetActiveID(window->MoveId, window); + g.NavDisableHighlight = true; + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindow->Pos; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingAllKeyboardKeys(); + + bool can_move_window = true; + if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindow->Flags & ImGuiWindowFlags_NoMove)) + can_move_window = false; + if (can_move_window) + g.MovingWindow = window; } -// Close the popup we have begin-ed into. -void ImGui::CloseCurrentPopup() +// Handle mouse moving window +// Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing() +// FIXME: We don't have strong guarantee that g.MovingWindow stay synched with g.ActiveId == g.MovingWindow->MoveId. +// This is currently enforced by the fact that BeginDragDropSource() is setting all g.ActiveIdUsingXXXX flags to inhibit navigation inputs, +// but if we should more thoroughly test cases where g.ActiveId or g.MovingWindow gets changed and not the other. +void ImGui::UpdateMouseMovingWindowNewFrame() { - ImGuiContext &g = *GImGui; - int popup_idx = g.CurrentPopupStack.Size - 1; - if (popup_idx < 0 || popup_idx >= g.OpenPopupStack.Size || g.CurrentPopupStack[popup_idx].PopupId != g.OpenPopupStack[popup_idx].PopupId) - return; - while (popup_idx > 0 && g.OpenPopupStack[popup_idx].Window && (g.OpenPopupStack[popup_idx].Window->Flags & ImGuiWindowFlags_ChildMenu)) - popup_idx--; - ClosePopupToLevel(popup_idx); + ImGuiContext& g = *GImGui; + if (g.MovingWindow != NULL) + { + // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). + // We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. + KeepAliveID(g.ActiveId); + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); + ImGuiWindow* moving_window = g.MovingWindow->RootWindow; + if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos)) + { + ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + SetWindowPos(moving_window, pos, ImGuiCond_Always); + FocusWindow(g.MovingWindow); + } + else + { + g.MovingWindow = NULL; + ClearActiveID(); + } + } + else + { + // When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others. + if (g.ActiveIdWindow && g.ActiveIdWindow->MoveId == g.ActiveId) + { + KeepAliveID(g.ActiveId); + if (!g.IO.MouseDown[0]) + ClearActiveID(); + } + } } -bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) +// Initiate moving window when clicking on empty space or title bar. +// Handle left-click and right-click focus. +void ImGui::UpdateMouseMovingWindowEndFrame() { - ImGuiContext &g = *GImGui; - if (!IsPopupOpen(id)) - { - g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values - return false; - } + ImGuiContext& g = *GImGui; + if (g.ActiveId != 0 || g.HoveredId != 0) + return; - char name[20]; - if (extra_flags & ImGuiWindowFlags_ChildMenu) - ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth - else - ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame + // Unless we just made a window/popup appear + if (g.NavWindow && g.NavWindow->Appearing) + return; - bool is_open = Begin(name, NULL, extra_flags | ImGuiWindowFlags_Popup); - if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) - EndPopup(); + // Click on empty space to focus window and start moving + // (after we're done with all our widgets) + if (g.IO.MouseClicked[0]) + { + // Handle the edge case of a popup being closed while clicking in its empty space. + // If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more. + ImGuiWindow* root_window = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + const bool is_closed_popup = root_window && (root_window->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(root_window->PopupId, ImGuiPopupFlags_AnyPopupLevel); + + if (root_window != NULL && !is_closed_popup) + { + StartMouseMovingWindow(g.HoveredWindow); //-V595 + + // Cancel moving if clicked outside of title bar + if (g.IO.ConfigWindowsMoveFromTitleBarOnly) + if (!(root_window->Flags & ImGuiWindowFlags_NoTitleBar)) + if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) + g.MovingWindow = NULL; + + // Cancel moving if clicked over an item which was disabled or inhibited by popups (note that we know HoveredId == 0 already) + if (g.HoveredIdIsDisabled) + g.MovingWindow = NULL; + } + else if (root_window == NULL && g.NavWindow != NULL) + { + // Clicking on void disable focus + FocusWindow(NULL, ImGuiFocusRequestFlags_UnlessBelowModal); + } + } - return is_open; + // With right mouse button we close popups without changing focus based on where the mouse is aimed + // Instead, focus will be restored to the window under the bottom-most closed popup. + // (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger) + if (g.IO.MouseClicked[1]) + { + // Find the top-most window between HoveredWindow and the top-most Modal Window. + // This is where we can trim the popup stack. + ImGuiWindow* modal = GetTopMostPopupModal(); + bool hovered_window_above_modal = g.HoveredWindow && (modal == NULL || IsWindowAbove(g.HoveredWindow, modal)); + ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal, true); + } } -bool ImGui::BeginPopup(const char *str_id, ImGuiWindowFlags flags) +static bool IsWindowActiveAndVisible(ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - if (g.OpenPopupStack.Size <= g.CurrentPopupStack.Size) // Early out for performance - { - g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values - return false; - } - return BeginPopupEx(g.CurrentWindow->GetID(str_id), flags | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); + return (window->Active) && (!window->Hidden); } -bool ImGui::IsPopupOpen(ImGuiID id) +// The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app) +void ImGui::UpdateHoveredWindowAndCaptureFlags() { - ImGuiContext &g = *GImGui; - return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == id; -} + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; -bool ImGui::IsPopupOpen(const char *str_id) -{ - ImGuiContext &g = *GImGui; - return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == g.CurrentWindow->GetID(str_id); -} + // FIXME-DPI: This storage was added on 2021/03/31 for test engine, but if we want to multiply WINDOWS_HOVER_PADDING + // by DpiScale, we need to make this window-agnostic anyhow, maybe need storing inside ImGuiWindow. + g.WindowsHoverPadding = ImMax(g.Style.TouchExtraPadding, ImVec2(WINDOWS_HOVER_PADDING, WINDOWS_HOVER_PADDING)); -bool ImGui::BeginPopupModal(const char *name, bool *p_open, ImGuiWindowFlags flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - const ImGuiID id = window->GetID(name); - if (!IsPopupOpen(id)) - { - g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values - return false; - } + // Find the window hovered by mouse: + // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. + // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point of the frame. + // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. + bool clear_hovered_windows = false; + FindHoveredWindowEx(g.IO.MousePos, false, &g.HoveredWindow, &g.HoveredWindowUnderMovingWindow); + g.HoveredWindowBeforeClear = g.HoveredWindow; - // Center modal windows by default - // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. - if (g.NextWindowData.PosCond == 0) - SetNextWindowPos(g.IO.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + // Modal windows prevents mouse from hovering behind them. + ImGuiWindow* modal_window = GetTopMostPopupModal(); + if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) + clear_hovered_windows = true; - bool is_open = Begin(name, p_open, flags | ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); - if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) - { - EndPopup(); - if (is_open) - ClosePopup(id); - return false; - } + // Disabled mouse hovering (we don't currently clear MousePos, we could) + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) + clear_hovered_windows = true; - return is_open; -} + // We track click ownership. When clicked outside of a window the click is owned by the application and + // won't report hovering nor request capture even while dragging over our windows afterward. + const bool has_open_popup = (g.OpenPopupStack.Size > 0); + const bool has_open_modal = (modal_window != NULL); + int mouse_earliest_down = -1; + bool mouse_any_down = false; + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + if (io.MouseClicked[i]) + { + io.MouseDownOwned[i] = (g.HoveredWindow != NULL) || has_open_popup; + io.MouseDownOwnedUnlessPopupClose[i] = (g.HoveredWindow != NULL) || has_open_modal; + } + mouse_any_down |= io.MouseDown[i]; + if (io.MouseDown[i] || io.MouseReleased[i]) // Increase release frame for our evaluation of earliest button (#1392) + if (mouse_earliest_down == -1 || io.MouseClickedTime[i] < io.MouseClickedTime[mouse_earliest_down]) + mouse_earliest_down = i; + } + const bool mouse_avail = (mouse_earliest_down == -1) || io.MouseDownOwned[mouse_earliest_down]; + const bool mouse_avail_unless_popup_close = (mouse_earliest_down == -1) || io.MouseDownOwnedUnlessPopupClose[mouse_earliest_down]; -static void NavProcessMoveRequestWrapAround(ImGuiWindow *window) -{ - ImGuiContext &g = *GImGui; - if (g.NavWindow == window && NavMoveRequestButNoResultYet()) - if ((g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) && g.NavMoveRequestForward == ImGuiNavForward_None && g.NavLayer == 0) - { - g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; - ImGui::NavMoveRequestCancel(); - g.NavWindow->NavRectRel[0].Min.y = g.NavWindow->NavRectRel[0].Max.y = ((g.NavMoveDir == ImGuiDir_Up) ? ImMax(window->SizeFull.y, window->SizeContents.y) : 0.0f) - window->Scroll.y; - } -} + // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. + // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) + const bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; + if (!mouse_avail && !mouse_dragging_extern_payload) + clear_hovered_windows = true; -void ImGui::EndPopup() -{ - ImGuiContext &g = *GImGui; - (void) g; - IM_ASSERT(g.CurrentWindow->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls - IM_ASSERT(g.CurrentPopupStack.Size > 0); + if (clear_hovered_windows) + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; - // Make all menus and popups wrap around for now, may need to expose that policy. - NavProcessMoveRequestWrapAround(g.CurrentWindow); + // Update io.WantCaptureMouse for the user application (true = dispatch mouse info to Dear ImGui only, false = dispatch mouse to Dear ImGui + underlying app) + // Update io.WantCaptureMouseAllowPopupClose (experimental) to give a chance for app to react to popup closure with a drag + if (g.WantCaptureMouseNextFrame != -1) + { + io.WantCaptureMouse = io.WantCaptureMouseUnlessPopupClose = (g.WantCaptureMouseNextFrame != 0); + } + else + { + io.WantCaptureMouse = (mouse_avail && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_popup; + io.WantCaptureMouseUnlessPopupClose = (mouse_avail_unless_popup_close && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_modal; + } - End(); + // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app) + io.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); + if (io.NavActive && (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && !(io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard)) + io.WantCaptureKeyboard = true; + if (g.WantCaptureKeyboardNextFrame != -1) // Manual override + io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); + + // Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible + io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; +} + +// Calling SetupDrawListSharedData() is followed by SetCurrentFont() which sets up the remaining data. +static void SetupDrawListSharedData() +{ + ImGuiContext& g = *GImGui; + ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiViewportP* viewport : g.Viewports) + virtual_space.Add(viewport->GetMainRect()); + g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); + g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; + if (g.Style.AntiAliasedLines) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; + if (g.Style.AntiAliasedLinesUseTex && !(g.IO.Fonts->Flags & ImFontAtlasFlags_NoBakedLines)) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; + if (g.Style.AntiAliasedFill) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; + if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; } -bool ImGui::OpenPopupOnItemClick(const char *str_id, int mouse_button) +void ImGui::NewFrame() { - ImGuiWindow *window = GImGui->CurrentWindow; - if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - { - ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! - IM_ASSERT(id != 0); // However, you cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) - OpenPopupEx(id); - return true; - } - return false; -} + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + ImGuiContext& g = *GImGui; + + // Remove pending delete hooks before frame start. + // This deferred removal avoid issues of removal while iterating the hook vector + for (int n = g.Hooks.Size - 1; n >= 0; n--) + if (g.Hooks[n].Type == ImGuiContextHookType_PendingRemoval_) + g.Hooks.erase(&g.Hooks[n]); + + CallContextHooks(&g, ImGuiContextHookType_NewFramePre); + + // Check and assert for various common IO and Configuration mistakes + ErrorCheckNewFrameSanityChecks(); + + // Load settings on first frame, save settings when modified (after a delay) + UpdateSettings(); + + g.Time += g.IO.DeltaTime; + g.WithinFrameScope = true; + g.FrameCount += 1; + g.TooltipOverrideCount = 0; + g.WindowsActiveCount = 0; + g.MenusIdSubmittedThisFrame.resize(0); + + // Calculate frame-rate for the user, as a purely luxurious feature + g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; + g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; + g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); + g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, IM_ARRAYSIZE(g.FramerateSecPerFrame)); + g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)g.FramerateSecPerFrameCount)) : FLT_MAX; + + // Process input queue (trickle as many events as possible), turn events into writes to IO structure + g.InputEventsTrail.resize(0); + UpdateInputEvents(g.IO.ConfigInputTrickleEventQueue); + + // Update viewports (after processing input queue, so io.MouseHoveredViewport is set) + UpdateViewportsNewFrame(); + + // Setup current font and draw list shared data + g.IO.Fonts->Locked = true; + SetupDrawListSharedData(); + SetCurrentFont(GetDefaultFont()); + IM_ASSERT(g.Font->IsLoaded()); + + // Mark rendering data as invalid to prevent user who may have a handle on it to use it. + for (ImGuiViewportP* viewport : g.Viewports) + viewport->DrawDataP.Valid = false; + + // Drag and drop keep the source ID alive so even if the source disappear our state is consistent + if (g.DragDropActive && g.DragDropPayload.SourceId == g.ActiveId) + KeepAliveID(g.DragDropPayload.SourceId); + + // Update HoveredId data + if (!g.HoveredIdPreviousFrame) + g.HoveredIdTimer = 0.0f; + if (!g.HoveredIdPreviousFrame || (g.HoveredId && g.ActiveId == g.HoveredId)) + g.HoveredIdNotActiveTimer = 0.0f; + if (g.HoveredId) + g.HoveredIdTimer += g.IO.DeltaTime; + if (g.HoveredId && g.ActiveId != g.HoveredId) + g.HoveredIdNotActiveTimer += g.IO.DeltaTime; + g.HoveredIdPreviousFrame = g.HoveredId; + g.HoveredId = 0; + g.HoveredIdAllowOverlap = false; + g.HoveredIdIsDisabled = false; + + // Clear ActiveID if the item is not alive anymore. + // In 1.87, the common most call to KeepAliveID() was moved from GetID() to ItemAdd(). + // As a result, custom widget using ButtonBehavior() _without_ ItemAdd() need to call KeepAliveID() themselves. + if (g.ActiveId != 0 && g.ActiveIdIsAlive != g.ActiveId && g.ActiveIdPreviousFrame == g.ActiveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("NewFrame(): ClearActiveID() because it isn't marked alive anymore!\n"); + ClearActiveID(); + } -// This is a helper to handle the simplest case of associating one named popup to one given widget. -// You may want to handle this on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). -// You can pass a NULL str_id to use the identifier of the last item. -bool ImGui::BeginPopupContextItem(const char *str_id, int mouse_button) -{ - ImGuiWindow *window = GImGui->CurrentWindow; - ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! - IM_ASSERT(id != 0); // However, you cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) - if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); -} + // Update ActiveId data (clear reference to active widget if the widget isn't alive anymore) + if (g.ActiveId) + g.ActiveIdTimer += g.IO.DeltaTime; + g.LastActiveIdTimer += g.IO.DeltaTime; + g.ActiveIdPreviousFrame = g.ActiveId; + g.ActiveIdPreviousFrameWindow = g.ActiveIdWindow; + g.ActiveIdPreviousFrameHasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore; + g.ActiveIdIsAlive = 0; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdPreviousFrameIsAlive = false; + g.ActiveIdIsJustActivated = false; + if (g.TempInputId != 0 && g.ActiveId != g.TempInputId) + g.TempInputId = 0; + if (g.ActiveId == 0) + { + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; + } -bool ImGui::BeginPopupContextWindow(const char *str_id, int mouse_button, bool also_over_items) -{ - if (!str_id) - str_id = "window_context"; - ImGuiID id = GImGui->CurrentWindow->GetID(str_id); - if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - if (also_over_items || !IsAnyItemHovered()) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); -} + // Record when we have been stationary as this state is preserved while over same item. + // FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values. + // To allow this we should store HoverItemMaxStationaryTime+ID and perform the >= check in IsItemHovered() function. + if (g.HoverItemDelayId != 0 && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverItemUnlockedStationaryId = g.HoverItemDelayId; + else if (g.HoverItemDelayId == 0) + g.HoverItemUnlockedStationaryId = 0; + if (g.HoveredWindow != NULL && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverWindowUnlockedStationaryId = g.HoveredWindow->ID; + else if (g.HoveredWindow == NULL) + g.HoverWindowUnlockedStationaryId = 0; + + // Update hover delay for IsItemHovered() with delays and tooltips + g.HoverItemDelayIdPreviousFrame = g.HoverItemDelayId; + if (g.HoverItemDelayId != 0) + { + g.HoverItemDelayTimer += g.IO.DeltaTime; + g.HoverItemDelayClearTimer = 0.0f; + g.HoverItemDelayId = 0; + } + else if (g.HoverItemDelayTimer > 0.0f) + { + // This gives a little bit of leeway before clearing the hover timer, allowing mouse to cross gaps + // We could expose 0.25f as style.HoverClearDelay but I am not sure of the logic yet, this is particularly subtle. + g.HoverItemDelayClearTimer += g.IO.DeltaTime; + if (g.HoverItemDelayClearTimer >= ImMax(0.25f, g.IO.DeltaTime * 2.0f)) // ~7 frames at 30 Hz + allow for low framerate + g.HoverItemDelayTimer = g.HoverItemDelayClearTimer = 0.0f; // May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer. + } -bool ImGui::BeginPopupContextVoid(const char *str_id, int mouse_button) -{ - if (!str_id) - str_id = "void_context"; - ImGuiID id = GImGui->CurrentWindow->GetID(str_id); - if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); -} + // Drag and drop + g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; + g.DragDropAcceptIdCurr = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropWithinSource = false; + g.DragDropWithinTarget = false; + g.DragDropHoldJustPressedId = 0; -static bool BeginChildEx(const char *name, ImGuiID id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *parent_window = ImGui::GetCurrentWindow(); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_ChildWindow; - flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + // Close popups on focus lost (currently wip/opt-in) + //if (g.IO.AppFocusLost) + // ClosePopupsExceptModals(); + + // Update keyboard input state + UpdateKeyboardInputs(); + + //IM_ASSERT(g.IO.KeyCtrl == IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl)); + //IM_ASSERT(g.IO.KeyShift == IsKeyDown(ImGuiKey_LeftShift) || IsKeyDown(ImGuiKey_RightShift)); + //IM_ASSERT(g.IO.KeyAlt == IsKeyDown(ImGuiKey_LeftAlt) || IsKeyDown(ImGuiKey_RightAlt)); + //IM_ASSERT(g.IO.KeySuper == IsKeyDown(ImGuiKey_LeftSuper) || IsKeyDown(ImGuiKey_RightSuper)); + + // Update gamepad/keyboard navigation + NavUpdate(); + + // Update mouse input state + UpdateMouseInputs(); + + // Find hovered window + // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) + UpdateHoveredWindowAndCaptureFlags(); + + // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) + UpdateMouseMovingWindowNewFrame(); - const ImVec2 content_avail = ImGui::GetContentRegionAvail(); - ImVec2 size = ImFloor(size_arg); - const int auto_fit_axises = ((size.x == 0.0f) ? (1 << ImGuiAxis_X) : 0x00) | ((size.y == 0.0f) ? (1 << ImGuiAxis_Y) : 0x00); - if (size.x <= 0.0f) - size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) - if (size.y <= 0.0f) - size.y = ImMax(content_avail.y + size.y, 4.0f); + // Background darkening/whitening + if (GetTopMostPopupModal() != NULL || (g.NavWindowingTarget != NULL && g.NavWindowingHighlightAlpha > 0.0f)) + g.DimBgRatio = ImMin(g.DimBgRatio + g.IO.DeltaTime * 6.0f, 1.0f); + else + g.DimBgRatio = ImMax(g.DimBgRatio - g.IO.DeltaTime * 10.0f, 0.0f); - const float backup_border_size = g.Style.ChildBorderSize; - if (!border) - g.Style.ChildBorderSize = 0.0f; - flags |= extra_flags; + g.MouseCursor = ImGuiMouseCursor_Arrow; + g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; - char title[256]; - if (name) - ImFormatString(title, IM_ARRAYSIZE(title), "%s/%s_%08X", parent_window->Name, name, id); - else - ImFormatString(title, IM_ARRAYSIZE(title), "%s/%08X", parent_window->Name, id); + // Platform IME data: reset for the frame + g.PlatformImeDataPrev = g.PlatformImeData; + g.PlatformImeData.WantVisible = false; - ImGui::SetNextWindowSize(size); - bool ret = ImGui::Begin(title, NULL, flags); - ImGuiWindow *child_window = ImGui::GetCurrentWindow(); - child_window->ChildId = id; - child_window->AutoFitChildAxises = auto_fit_axises; - g.Style.ChildBorderSize = backup_border_size; + // Mouse wheel scrolling, scale + UpdateMouseWheel(); - // Process navigation-in immediately so NavInit can run on first frame - if (!(flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayerActiveMask != 0 || child_window->DC.NavHasScroll) && g.NavActivateId == id) - { - ImGui::FocusWindow(child_window); - ImGui::NavInitWindow(child_window, false); - ImGui::SetActiveID(id + 1, child_window); // Steal ActiveId with a dummy id so that key-press won't activate child item - g.ActiveIdSource = ImGuiInputSource_Nav; - } + // Mark all windows as not visible and compact unused memory. + IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); + const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; + for (ImGuiWindow* window : g.Windows) + { + window->WasActive = window->Active; + window->Active = false; + window->WriteAccessed = false; + window->BeginCountPreviousFrame = window->BeginCount; + window->BeginCount = 0; + + // Garbage collect transient buffers of recently unused windows + if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) + GcCompactTransientWindowBuffers(window); + } + + // Garbage collect transient buffers of recently unused tables + for (int i = 0; i < g.TablesLastTimeActive.Size; i++) + if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) + TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); + for (ImGuiTableTempData& table_temp_data : g.TablesTempData) + if (table_temp_data.LastTimeActive >= 0.0f && table_temp_data.LastTimeActive < memory_compact_start_time) + TableGcCompactTransientBuffers(&table_temp_data); + if (g.GcCompactAll) + GcCompactTransientMiscBuffers(); + g.GcCompactAll = false; + + // Closing the focused window restore focus to the first active root window in descending z-order + if (g.NavWindow && !g.NavWindow->WasActive) + FocusTopMostWindowUnderOne(NULL, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); + + // No window should be open at the beginning of the frame. + // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. + g.CurrentWindowStack.resize(0); + g.BeginPopupStack.resize(0); + g.ItemFlagsStack.resize(0); + g.ItemFlagsStack.push_back(ImGuiItemFlags_AutoClosePopups); // Default flags + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.GroupStack.resize(0); + + // [DEBUG] Update debug features +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + UpdateDebugToolItemPicker(); + UpdateDebugToolStackQueries(); + UpdateDebugToolFlashStyleColor(); + if (g.DebugLocateFrames > 0 && --g.DebugLocateFrames == 0) + { + g.DebugLocateId = 0; + g.DebugBreakInLocateId = false; + } + if (g.DebugLogAutoDisableFrames > 0 && --g.DebugLogAutoDisableFrames == 0) + { + DebugLog("(Debug Log: Auto-disabled some ImGuiDebugLogFlags after 2 frames)\n"); + g.DebugLogFlags &= ~g.DebugLogAutoDisableFlags; + g.DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + } +#endif - return ret; + // Create implicit/fallback window - which we will only render it if the user has added something to it. + // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. + // This fallback is particularly important as it prevents ImGui:: calls from crashing. + g.WithinFrameScopeWithImplicitWindow = true; + SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver); + Begin("Debug##Default"); + IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true); + + // [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack, + // allowing to validate correct Begin/End behavior in user code. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.IO.ConfigDebugBeginReturnValueLoop) + g.DebugBeginReturnValueCullDepth = (g.DebugBeginReturnValueCullDepth == -1) ? 0 : ((g.DebugBeginReturnValueCullDepth + ((g.FrameCount % 4) == 0 ? 1 : 0)) % 10); + else + g.DebugBeginReturnValueCullDepth = -1; +#endif + + CallContextHooks(&g, ImGuiContextHookType_NewFramePost); } -bool ImGui::BeginChild(const char *str_id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_flags) +// FIXME: Add a more explicit sort order in the window structure. +static int IMGUI_CDECL ChildWindowComparer(const void* lhs, const void* rhs) { - ImGuiWindow *window = GetCurrentWindow(); - return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); + const ImGuiWindow* const a = *(const ImGuiWindow* const *)lhs; + const ImGuiWindow* const b = *(const ImGuiWindow* const *)rhs; + if (int d = (a->Flags & ImGuiWindowFlags_Popup) - (b->Flags & ImGuiWindowFlags_Popup)) + return d; + if (int d = (a->Flags & ImGuiWindowFlags_Tooltip) - (b->Flags & ImGuiWindowFlags_Tooltip)) + return d; + return (a->BeginOrderWithinParent - b->BeginOrderWithinParent); } -bool ImGui::BeginChild(ImGuiID id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_flags) +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window) { - IM_ASSERT(id != 0); - return BeginChildEx(NULL, id, size_arg, border, extra_flags); + out_sorted_windows->push_back(window); + if (window->Active) + { + int count = window->DC.ChildWindows.Size; + ImQsort(window->DC.ChildWindows.Data, (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); + for (int i = 0; i < count; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (child->Active) + AddWindowToSortBuffer(out_sorted_windows, child); + } + } } -void ImGui::EndChild() +static void AddWindowToDrawData(ImGuiWindow* window, int layer) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - IM_ASSERT(window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() callss - if (window->BeginCount > 1) - { - End(); - } - else - { - // When using auto-filling child window, we don't provide full width/height to ItemSize so that it doesn't feed back into automatic size-fitting. - ImVec2 sz = GetWindowSize(); - if (window->AutoFitChildAxises & (1 << ImGuiAxis_X)) // Arbitrary minimum zero-ish child size of 4.0f causes less trouble than a 0.0f - sz.x = ImMax(4.0f, sz.x); - if (window->AutoFitChildAxises & (1 << ImGuiAxis_Y)) - sz.y = ImMax(4.0f, sz.y); - End(); - - ImGuiWindow *parent_window = g.CurrentWindow; - ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + sz); - ItemSize(sz); - if ((window->DC.NavLayerActiveMask != 0 || window->DC.NavHasScroll) && !(window->Flags & ImGuiWindowFlags_NavFlattened)) - { - ItemAdd(bb, window->ChildId); - RenderNavHighlight(bb, window->ChildId); - - // When browsing a window that has no activable items (scroll only) we keep a highlight on the child - if (window->DC.NavLayerActiveMask == 0 && window == g.NavWindow) - RenderNavHighlight(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavHighlightFlags_TypeThin); - } - else - { - // Not navigable into - ItemAdd(bb, 0); - } - } -} - -// Helper to create a child window / scrolling region that looks like a normal widget frame. -bool ImGui::BeginChildFrame(ImGuiID id, const ImVec2 &size, ImGuiWindowFlags extra_flags) -{ - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); - PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); - PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); - PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); - return BeginChild(id, size, true, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysUseWindowPadding | extra_flags); -} - -void ImGui::EndChildFrame() -{ - EndChild(); - PopStyleVar(3); - PopStyleColor(); -} - -// Save and compare stack sizes on Begin()/End() to detect usage errors -static void CheckStacksSize(ImGuiWindow *window, bool write) -{ - // NOT checking: DC.ItemWidth, DC.AllowKeyboardFocus, DC.ButtonRepeat, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) - ImGuiContext &g = *GImGui; - int *p_backup = &window->DC.StackSizesBackup[0]; - { - int current = window->IDStack.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "PushID/PopID or TreeNode/TreePop Mismatch!"); - p_backup++; - } // Too few or too many PopID()/TreePop() - { - int current = window->DC.GroupStack.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "BeginGroup/EndGroup Mismatch!"); - p_backup++; - } // Too few or too many EndGroup() - { - int current = g.CurrentPopupStack.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "BeginMenu/EndMenu or BeginPopup/EndPopup Mismatch"); - p_backup++; - } // Too few or too many EndMenu()/EndPopup() - { - int current = g.ColorModifiers.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "PushStyleColor/PopStyleColor Mismatch!"); - p_backup++; - } // Too few or too many PopStyleColor() - { - int current = g.StyleModifiers.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "PushStyleVar/PopStyleVar Mismatch!"); - p_backup++; - } // Too few or too many PopStyleVar() - { - int current = g.FontStack.Size; - if (write) - *p_backup = current; - else - IM_ASSERT(*p_backup == current && "PushFont/PopFont Mismatch!"); - p_backup++; - } // Too few or too many PopFont() - IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); -} - -enum ImGuiPopupPositionPolicy -{ - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox -}; - -static ImVec2 FindBestWindowPosForPopup(const ImVec2 &ref_pos, const ImVec2 &size, ImGuiDir *last_dir, const ImRect &r_avoid, ImGuiPopupPositionPolicy policy = ImGuiPopupPositionPolicy_Default) -{ - const ImGuiStyle &style = GImGui->Style; - - // r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) - // r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. - ImVec2 safe_padding = style.DisplaySafeAreaPadding; - ImRect r_outer(GetViewportRect()); - r_outer.Expand(ImVec2((size.x - r_outer.GetWidth() > safe_padding.x * 2) ? -safe_padding.x : 0.0f, (size.y - r_outer.GetHeight() > safe_padding.y * 2) ? -safe_padding.y : 0.0f)); - ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); - // GImGui->OverlayDrawList.AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); - // GImGui->OverlayDrawList.AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); - - // Combo Box policy (we want a connecting edge) - if (policy == ImGuiPopupPositionPolicy_ComboBox) - { - const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = {ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up}; - for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) - { - const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; - if (n != -1 && dir == *last_dir) // Already tried this direction? - continue; - ImVec2 pos; - if (dir == ImGuiDir_Down) - pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) - if (dir == ImGuiDir_Right) - pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right - if (dir == ImGuiDir_Left) - pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left - if (dir == ImGuiDir_Up) - pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left - if (!r_outer.Contains(ImRect(pos, pos + size))) - continue; - *last_dir = dir; - return pos; - } - } - - // Default popup policy - const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = {ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left}; - for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) - { - const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; - if (n != -1 && dir == *last_dir) // Already tried this direction? - continue; - float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); - float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); - if (avail_w < size.x || avail_h < size.y) - continue; - ImVec2 pos; - pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : - base_pos_clamped.x; - pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : - base_pos_clamped.y; - *last_dir = dir; - return pos; - } - - // Fallback, try to keep within display - *last_dir = ImGuiDir_None; - ImVec2 pos = ref_pos; - pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); - pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); - return pos; -} - -static void SetWindowConditionAllowFlags(ImGuiWindow *window, ImGuiCond flags, bool enabled) -{ - window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); - window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); - window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); -} - -ImGuiWindow *ImGui::FindWindowByName(const char *name) -{ - ImGuiContext &g = *GImGui; - ImGuiID id = ImHash(name, 0); - return (ImGuiWindow *) g.WindowsById.GetVoidPtr(id); -} - -static ImGuiWindow *CreateNewWindow(const char *name, ImVec2 size, ImGuiWindowFlags flags) -{ - ImGuiContext &g = *GImGui; - - // Create window the first time - ImGuiWindow *window = IM_NEW(ImGuiWindow)(&g, name); - window->Flags = flags; - g.WindowsById.SetVoidPtr(window->ID, window); - - // User can disable loading and saving of settings. Tooltip and child windows also don't store settings. - if (!(flags & ImGuiWindowFlags_NoSavedSettings)) - { - // Retrieve settings from .ini file - // Use SetWindowPos() or SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. - window->Pos = window->PosFloat = ImVec2(60, 60); - - if (ImGuiWindowSettings *settings = ImGui::FindWindowSettings(window->ID)) - { - SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); - window->PosFloat = settings->Pos; - window->Pos = ImFloor(window->PosFloat); - window->Collapsed = settings->Collapsed; - if (ImLengthSqr(settings->Size) > 0.00001f) - size = settings->Size; - } - } - window->Size = window->SizeFull = window->SizeFullAtLastBegin = size; - - if ((flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) - { - window->AutoFitFramesX = window->AutoFitFramesY = 2; - window->AutoFitOnlyGrows = false; - } - else - { - if (window->Size.x <= 0.0f) - window->AutoFitFramesX = 2; - if (window->Size.y <= 0.0f) - window->AutoFitFramesY = 2; - window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); - } - - if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) - g.Windows.insert(g.Windows.begin(), window); // Quite slow but rare and only once - else - g.Windows.push_back(window); - return window; -} - -static ImVec2 CalcSizeAfterConstraint(ImGuiWindow *window, ImVec2 new_size) -{ - ImGuiContext &g = *GImGui; - if (g.NextWindowData.SizeConstraintCond != 0) - { - // Using -1,-1 on either X/Y axis to preserve the current size. - ImRect cr = g.NextWindowData.SizeConstraintRect; - new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; - new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; - if (g.NextWindowData.SizeCallback) - { - ImGuiSizeCallbackData data; - data.UserData = g.NextWindowData.SizeCallbackUserData; - data.Pos = window->Pos; - data.CurrentSize = window->SizeFull; - data.DesiredSize = new_size; - g.NextWindowData.SizeCallback(&data); - new_size = data.DesiredSize; - } - } - - // Minimum size - if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) - { - new_size = ImMax(new_size, g.Style.WindowMinSize); - new_size.y = ImMax(new_size.y, window->TitleBarHeight() + window->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows - } - return new_size; -} - -static ImVec2 CalcSizeContents(ImGuiWindow *window) -{ - ImVec2 sz; - sz.x = (float) (int) ((window->SizeContentsExplicit.x != 0.0f) ? window->SizeContentsExplicit.x : (window->DC.CursorMaxPos.x - window->Pos.x + window->Scroll.x)); - sz.y = (float) (int) ((window->SizeContentsExplicit.y != 0.0f) ? window->SizeContentsExplicit.y : (window->DC.CursorMaxPos.y - window->Pos.y + window->Scroll.y)); - return sz + window->WindowPadding; -} - -static ImVec2 CalcSizeAutoFit(ImGuiWindow *window, const ImVec2 &size_contents) -{ - ImGuiContext &g = *GImGui; - ImGuiStyle &style = g.Style; - ImGuiWindowFlags flags = window->Flags; - ImVec2 size_auto_fit; - if ((flags & ImGuiWindowFlags_Tooltip) != 0) - { - // Tooltip always resize. We keep the spacing symmetric on both axises for aesthetic purpose. - size_auto_fit = size_contents; - } - else - { - // When the window cannot fit all contents (either because of constraints, either because screen is too small): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. - size_auto_fit = ImClamp(size_contents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); - ImVec2 size_auto_fit_after_constraint = CalcSizeAfterConstraint(window, size_auto_fit); - if (size_auto_fit_after_constraint.x < size_contents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) - size_auto_fit.y += style.ScrollbarSize; - if (size_auto_fit_after_constraint.y < size_contents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) - size_auto_fit.x += style.ScrollbarSize; - } - return size_auto_fit; -} - -static float GetScrollMaxX(ImGuiWindow *window) -{ - return ImMax(0.0f, window->SizeContents.x - (window->SizeFull.x - window->ScrollbarSizes.x)); -} - -static float GetScrollMaxY(ImGuiWindow *window) -{ - return ImMax(0.0f, window->SizeContents.y - (window->SizeFull.y - window->ScrollbarSizes.y)); -} - -static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow *window) -{ - ImVec2 scroll = window->Scroll; - float cr_x = window->ScrollTargetCenterRatio.x; - float cr_y = window->ScrollTargetCenterRatio.y; - if (window->ScrollTarget.x < FLT_MAX) - scroll.x = window->ScrollTarget.x - cr_x * (window->SizeFull.x - window->ScrollbarSizes.x); - if (window->ScrollTarget.y < FLT_MAX) - scroll.y = window->ScrollTarget.y - (1.0f - cr_y) * (window->TitleBarHeight() + window->MenuBarHeight()) - cr_y * (window->SizeFull.y - window->ScrollbarSizes.y); - scroll = ImMax(scroll, ImVec2(0.0f, 0.0f)); - if (!window->Collapsed && !window->SkipItems) - { - scroll.x = ImMin(scroll.x, GetScrollMaxX(window)); - scroll.y = ImMin(scroll.y, GetScrollMaxY(window)); - } - return scroll; -} - -static ImGuiCol GetWindowBgColorIdxFromFlags(ImGuiWindowFlags flags) -{ - if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) - return ImGuiCol_PopupBg; - if (flags & ImGuiWindowFlags_ChildWindow) - return ImGuiCol_ChildBg; - return ImGuiCol_WindowBg; -} - -static void CalcResizePosSizeFromAnyCorner(ImGuiWindow *window, const ImVec2 &corner_target, const ImVec2 &corner_norm, ImVec2 *out_pos, ImVec2 *out_size) -{ - ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left - ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right - ImVec2 size_expected = pos_max - pos_min; - ImVec2 size_constrained = CalcSizeAfterConstraint(window, size_expected); - *out_pos = pos_min; - if (corner_norm.x == 0.0f) - out_pos->x -= (size_constrained.x - size_expected.x); - if (corner_norm.y == 0.0f) - out_pos->y -= (size_constrained.y - size_expected.y); - *out_size = size_constrained; + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + g.IO.MetricsRenderWindows++; + if (window->DrawList->_Splitter._Count > 1) + window->DrawList->ChannelsMerge(); // Merge if user forgot to merge back. Also required in Docking branch for ImGuiWindowFlags_DockNodeHost windows. + ImGui::AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[layer], window->DrawList); + for (ImGuiWindow* child : window->DC.ChildWindows) + if (IsWindowActiveAndVisible(child)) // Clipped children may have been marked not active + AddWindowToDrawData(child, layer); } -struct ImGuiResizeGripDef -{ - ImVec2 CornerPos; - ImVec2 InnerDir; - int AngleMin12, AngleMax12; -}; - -const ImGuiResizeGripDef resize_grip_def[4] = - { - {ImVec2(1, 1), ImVec2(-1, -1), 0, 3}, // Lower right - {ImVec2(0, 1), ImVec2(+1, -1), 3, 6}, // Lower left - {ImVec2(0, 0), ImVec2(+1, +1), 6, 9}, // Upper left - {ImVec2(1, 0), ImVec2(-1, +1), 9, 12}, // Upper right -}; - -static ImRect GetBorderRect(ImGuiWindow *window, int border_n, float perp_padding, float thickness) +static inline int GetWindowDisplayLayer(ImGuiWindow* window) { - ImRect rect = window->Rect(); - if (thickness == 0.0f) - rect.Max -= ImVec2(1, 1); - if (border_n == 0) - return ImRect(rect.Min.x + perp_padding, rect.Min.y, rect.Max.x - perp_padding, rect.Min.y + thickness); - if (border_n == 1) - return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x, rect.Max.y - perp_padding); - if (border_n == 2) - return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y); - if (border_n == 3) - return ImRect(rect.Min.x, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); - IM_ASSERT(0); - return ImRect(); + return (window->Flags & ImGuiWindowFlags_Tooltip) ? 1 : 0; } -// Handle resize for: Resize Grips, Borders, Gamepad -static void ImGui::UpdateManualResize(ImGuiWindow *window, const ImVec2 &size_auto_fit, int *border_held, int resize_grip_count, ImU32 resize_grip_col[4]) -{ - ImGuiContext &g = *GImGui; - ImGuiWindowFlags flags = window->Flags; - if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) - return; - - const int resize_border_count = (flags & ImGuiWindowFlags_ResizeFromAnySide) ? 4 : 0; - const float grip_draw_size = (float) (int) ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); - const float grip_hover_size = (float) (int) (grip_draw_size * 0.75f); - - ImVec2 pos_target(FLT_MAX, FLT_MAX); - ImVec2 size_target(FLT_MAX, FLT_MAX); - - // Manual resize grips - PushID("#RESIZE"); - for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) - { - const ImGuiResizeGripDef &grip = resize_grip_def[resize_grip_n]; - const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPos); - - // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window - ImRect resize_rect(corner, corner + grip.InnerDir * grip_hover_size); - resize_rect.FixInverted(); - bool hovered, held; - ButtonBehavior(resize_rect, window->GetID((void *) (intptr_t) resize_grip_n), &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); - if (hovered || held) - g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; - - if (g.HoveredWindow == window && held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) - { - // Manual auto-fit when double-clicking - size_target = CalcSizeAfterConstraint(window, size_auto_fit); - ClearActiveID(); - } - else if (held) - { - // Resize from any of the four corners - // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position - ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + resize_rect.GetSize() * grip.CornerPos; // Corner of the window corresponding to our corner grip - CalcResizePosSizeFromAnyCorner(window, corner_target, grip.CornerPos, &pos_target, &size_target); - } - if (resize_grip_n == 0 || held || hovered) - resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : - ImGuiCol_ResizeGrip); - } - for (int border_n = 0; border_n < resize_border_count; border_n++) - { - const float BORDER_SIZE = 5.0f; // FIXME: Only works _inside_ window because of HoveredWindow check. - const float BORDER_APPEAR_TIMER = 0.05f; // Reduce visual noise - bool hovered, held; - ImRect border_rect = GetBorderRect(window, border_n, grip_hover_size, BORDER_SIZE); - ButtonBehavior(border_rect, window->GetID((void *) (intptr_t) (border_n + 4)), &hovered, &held, ImGuiButtonFlags_FlattenChildren); - if ((hovered && g.HoveredIdTimer > BORDER_APPEAR_TIMER) || held) - { - g.MouseCursor = (border_n & 1) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; - if (held) - *border_held = border_n; - } - if (held) - { - ImVec2 border_target = window->Pos; - ImVec2 border_posn; - if (border_n == 0) - { - border_posn = ImVec2(0, 0); - border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y); - } - if (border_n == 1) - { - border_posn = ImVec2(1, 0); - border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + BORDER_SIZE); - } - if (border_n == 2) - { - border_posn = ImVec2(0, 1); - border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y + BORDER_SIZE); - } - if (border_n == 3) - { - border_posn = ImVec2(0, 0); - border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x); - } - CalcResizePosSizeFromAnyCorner(window, border_target, border_posn, &pos_target, &size_target); - } - } - PopID(); - - // Navigation/gamepad resize - if (g.NavWindowingTarget == window) - { - ImVec2 nav_resize_delta; - if (g.NavWindowingInputSource == ImGuiInputSource_NavKeyboard && g.IO.KeyShift) - nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); - if (g.NavWindowingInputSource == ImGuiInputSource_NavGamepad) - nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_Down); - if (nav_resize_delta.x != 0.0f || nav_resize_delta.y != 0.0f) - { - const float NAV_RESIZE_SPEED = 600.0f; - nav_resize_delta *= ImFloor(NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); - g.NavWindowingToggleLayer = false; - g.NavDisableMouseHover = true; - resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); - // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. - size_target = CalcSizeAfterConstraint(window, window->SizeFull + nav_resize_delta); - } - } - - // Apply back modified position/size to window - if (size_target.x != FLT_MAX) - { - window->SizeFull = size_target; - MarkIniSettingsDirty(window); - } - if (pos_target.x != FLT_MAX) - { - window->Pos = window->PosFloat = ImFloor(pos_target); - MarkIniSettingsDirty(window); - } - - window->Size = window->SizeFull; -} - -// Push a new ImGui window to add widgets to. -// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. -// - Begin/End can be called multiple times during the frame with the same window name to append content. -// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). -// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. -// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. -// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. -bool ImGui::Begin(const char *name, bool *p_open, ImGuiWindowFlags flags) -{ - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - IM_ASSERT(name != NULL); // Window name required - IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() - IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet - - // Find or create - ImGuiWindow *window = FindWindowByName(name); - if (!window) - { - ImVec2 size_on_first_use = (g.NextWindowData.SizeCond != 0) ? g.NextWindowData.SizeVal : ImVec2(0.0f, 0.0f); // Any condition flag will do since we are creating a new window here. - window = CreateNewWindow(name, size_on_first_use, flags); - } - - // Automatically disable manual moving/resizing when NoInputs is set - if (flags & ImGuiWindowFlags_NoInputs) - flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; - - if (flags & ImGuiWindowFlags_NavFlattened) - IM_ASSERT(flags & ImGuiWindowFlags_ChildWindow); - - const int current_frame = g.FrameCount; - const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); - if (first_begin_of_the_frame) - window->Flags = (ImGuiWindowFlags) flags; - else - flags = window->Flags; - - // Update the Appearing flag - bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on - const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFrames == 1); - if (flags & ImGuiWindowFlags_Popup) - { - ImGuiPopupRef &popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; - window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed - window_just_activated_by_user |= (window != popup_ref.Window); - } - window->Appearing = (window_just_activated_by_user || window_just_appearing_after_hidden_for_resize); - window->CloseButton = (p_open != NULL); - if (window->Appearing) - SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); - - // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack - ImGuiWindow *parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back(); - ImGuiWindow *parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; - IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); - - // Add to stack - g.CurrentWindowStack.push_back(window); - SetCurrentWindow(window); - CheckStacksSize(window, true); - if (flags & ImGuiWindowFlags_Popup) - { - ImGuiPopupRef &popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; - popup_ref.Window = window; - g.CurrentPopupStack.push_back(popup_ref); - window->PopupId = popup_ref.PopupId; - } - - if (window_just_appearing_after_hidden_for_resize && !(flags & ImGuiWindowFlags_ChildWindow)) - window->NavLastIds[0] = 0; - - // Process SetNextWindow***() calls - bool window_pos_set_by_api = false; - bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; - if (g.NextWindowData.PosCond) - { - window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; - if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) - { - // May be processed on the next frame if this is our first frame and we are measuring size - // FIXME: Look into removing the branch so everything can go through this same code path for consistency. - window->SetWindowPosVal = g.NextWindowData.PosVal; - window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; - window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - } - else - { - SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); - } - g.NextWindowData.PosCond = 0; - } - if (g.NextWindowData.SizeCond) - { - window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); - window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); - SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); - g.NextWindowData.SizeCond = 0; - } - if (g.NextWindowData.ContentSizeCond) - { - // Adjust passed "client size" to become a "window size" - window->SizeContentsExplicit = g.NextWindowData.ContentSizeVal; - if (window->SizeContentsExplicit.y != 0.0f) - window->SizeContentsExplicit.y += window->TitleBarHeight() + window->MenuBarHeight(); - g.NextWindowData.ContentSizeCond = 0; - } - else if (first_begin_of_the_frame) - { - window->SizeContentsExplicit = ImVec2(0.0f, 0.0f); - } - if (g.NextWindowData.CollapsedCond) - { - SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); - g.NextWindowData.CollapsedCond = 0; - } - if (g.NextWindowData.FocusCond) - { - SetWindowFocus(); - g.NextWindowData.FocusCond = 0; - } - if (window->Appearing) - SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); - - // When reusing window again multiple times a frame, just append content (don't need to setup again) - if (first_begin_of_the_frame) - { - const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) - - // Initialize - window->ParentWindow = parent_window; - window->RootWindow = window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = window->RootWindowForNav = window; - if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !window_is_child_tooltip) - window->RootWindow = parent_window->RootWindow; - if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) - window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = parent_window->RootWindowForTitleBarHighlight; // Same value in master branch, will differ for docking - while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) - window->RootWindowForNav = window->RootWindowForNav->ParentWindow; - - window->Active = true; - window->BeginOrderWithinParent = 0; - window->BeginOrderWithinContext = g.WindowsActiveCount++; - window->BeginCount = 0; - window->ClipRect = ImVec4(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); - window->LastFrameActive = current_frame; - window->IDStack.resize(1); - - // Lock window rounding, border size and rounding so that altering the border sizes for children doesn't have side-effects. - window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : - style.WindowRounding; - window->WindowBorderSize = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildBorderSize : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : - style.WindowBorderSize; - window->WindowPadding = style.WindowPadding; - if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) - window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); - - // Collapse window by double-clicking on title bar - // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing - if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) - { - ImRect title_bar_rect = window->TitleBarRect(); - if (window->CollapseToggleWanted || (g.HoveredWindow == window && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseDoubleClicked[0])) - { - window->Collapsed = !window->Collapsed; - MarkIniSettingsDirty(window); - FocusWindow(window); - } - } - else - { - window->Collapsed = false; - } - window->CollapseToggleWanted = false; - - // SIZE - - // Update contents size from last frame for auto-fitting (unless explicitly specified) - window->SizeContents = CalcSizeContents(window); - - // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) - if (window->HiddenFrames > 0) - window->HiddenFrames--; - if ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0 && window_just_activated_by_user) - { - window->HiddenFrames = 1; - if (flags & ImGuiWindowFlags_AlwaysAutoResize) - { - if (!window_size_x_set_by_api) - window->Size.x = window->SizeFull.x = 0.f; - if (!window_size_y_set_by_api) - window->Size.y = window->SizeFull.y = 0.f; - window->SizeContents = ImVec2(0.f, 0.f); - } - } - - // Calculate auto-fit size, handle automatic resize - const ImVec2 size_auto_fit = CalcSizeAutoFit(window, window->SizeContents); - ImVec2 size_full_modified(FLT_MAX, FLT_MAX); - if (flags & ImGuiWindowFlags_AlwaysAutoResize && !window->Collapsed) - { - // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. - if (!window_size_x_set_by_api) - window->SizeFull.x = size_full_modified.x = size_auto_fit.x; - if (!window_size_y_set_by_api) - window->SizeFull.y = size_full_modified.y = size_auto_fit.y; - } - else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) - { - // Auto-fit only grows during the first few frames - // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. - if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) - window->SizeFull.x = size_full_modified.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; - if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) - window->SizeFull.y = size_full_modified.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; - if (!window->Collapsed) - MarkIniSettingsDirty(window); - } - - // Apply minimum/maximum window size constraints and final size - window->SizeFull = CalcSizeAfterConstraint(window, window->SizeFull); - window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; - - // SCROLLBAR STATUS - - // Update scrollbar status (based on the Size that was effective during last frame or the auto-resized Size). - if (!window->Collapsed) - { - // When reading the current size we need to read it after size constraints have been applied - float size_x_for_scrollbars = size_full_modified.x != FLT_MAX ? window->SizeFull.x : window->SizeFullAtLastBegin.x; - float size_y_for_scrollbars = size_full_modified.y != FLT_MAX ? window->SizeFull.y : window->SizeFullAtLastBegin.y; - window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); - window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); - if (window->ScrollbarX && !window->ScrollbarY) - window->ScrollbarY = (window->SizeContents.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); - window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); - } - - // POSITION - - // Popup latch its initial position, will position itself when it appears next frame - if (window_just_activated_by_user) - { - window->AutoPosLastDirection = ImGuiDir_None; - if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api) - window->Pos = window->PosFloat = g.CurrentPopupStack.back().OpenPopupPos; - } - - // Position child window - if (flags & ImGuiWindowFlags_ChildWindow) - { - window->BeginOrderWithinParent = parent_window->DC.ChildWindows.Size; - parent_window->DC.ChildWindows.push_back(window); - if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) - window->Pos = window->PosFloat = parent_window->DC.CursorPos; - } - - const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFrames == 0); - if (window_pos_with_pivot) - { - // Position given a pivot (e.g. for centering) - SetWindowPos(window, ImMax(style.DisplaySafeAreaPadding, window->SetWindowPosVal - window->SizeFull * window->SetWindowPosPivot), 0); - } - else if (flags & ImGuiWindowFlags_ChildMenu) - { - // Child menus typically request _any_ position within the parent menu item, and then our FindBestPopupWindowPos() function will move the new menu outside the parent bounds. - // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. - IM_ASSERT(window_pos_set_by_api); - float horizontal_overlap = style.ItemSpacing.x; // We want some overlap to convey the relative depth of each popup (currently the amount of overlap it is hard-coded to style.ItemSpacing.x, may need to introduce another style value). - ImGuiWindow *parent_menu = parent_window_in_stack; - ImRect rect_to_avoid; - if (parent_menu->DC.MenuBarAppending) - rect_to_avoid = ImRect(-FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight(), FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight() + parent_menu->MenuBarHeight()); - else - rect_to_avoid = ImRect(parent_menu->Pos.x + horizontal_overlap, -FLT_MAX, parent_menu->Pos.x + parent_menu->Size.x - horizontal_overlap - parent_menu->ScrollbarSizes.x, FLT_MAX); - window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); - } - else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) - { - ImRect rect_to_avoid(window->PosFloat.x - 1, window->PosFloat.y - 1, window->PosFloat.x + 1, window->PosFloat.y + 1); - window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); - } - - // Position tooltip (always follows mouse) - if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) - { - float sc = g.Style.MouseCursorScale; - ImVec2 ref_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; - ImRect rect_to_avoid; - if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.NavFlags & ImGuiNavFlags_MoveMouse)) - rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); - else - rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. - window->PosFloat = FindBestWindowPosForPopup(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid); - if (window->AutoPosLastDirection == ImGuiDir_None) - window->PosFloat = ref_pos + ImVec2(2, 2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. - } - - // Clamp position so it stays visible - if (!(flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) - { - if (!window_pos_set_by_api && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && g.IO.DisplaySize.x > 0.0f && g.IO.DisplaySize.y > 0.0f) // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. - { - ImVec2 padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); - window->PosFloat = ImMax(window->PosFloat + window->Size, padding) - window->Size; - window->PosFloat = ImMin(window->PosFloat, g.IO.DisplaySize - padding); - } - } - window->Pos = ImFloor(window->PosFloat); - - // Default item width. Make it proportional to window size if window manually resizes - if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) - window->ItemWidthDefault = (float) (int) (window->Size.x * 0.65f); - else - window->ItemWidthDefault = (float) (int) (g.FontSize * 16.0f); - - // Prepare for focus requests - window->FocusIdxAllRequestCurrent = (window->FocusIdxAllRequestNext == INT_MAX || window->FocusIdxAllCounter == -1) ? INT_MAX : (window->FocusIdxAllRequestNext + (window->FocusIdxAllCounter + 1)) % (window->FocusIdxAllCounter + 1); - window->FocusIdxTabRequestCurrent = (window->FocusIdxTabRequestNext == INT_MAX || window->FocusIdxTabCounter == -1) ? INT_MAX : (window->FocusIdxTabRequestNext + (window->FocusIdxTabCounter + 1)) % (window->FocusIdxTabCounter + 1); - window->FocusIdxAllCounter = window->FocusIdxTabCounter = -1; - window->FocusIdxAllRequestNext = window->FocusIdxTabRequestNext = INT_MAX; - - // Apply scrolling - window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); - window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); - - // Apply focus, new windows appears in front - bool want_focus = false; - if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) - if (!(flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) || (flags & ImGuiWindowFlags_Popup)) - want_focus = true; - - // Handle manual resize: Resize Grips, Borders, Gamepad - int border_held = -1; - ImU32 resize_grip_col[4] = {0}; - const int resize_grip_count = (flags & ImGuiWindowFlags_ResizeFromAnySide) ? 2 : 1; // 4 - const float grip_draw_size = (float) (int) ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); - if (!window->Collapsed) - UpdateManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0]); - - // DRAWING - - // Setup draw list and outer clipping rectangle - window->DrawList->Clear(); - window->DrawList->Flags = (g.Style.AntiAliasedLines ? ImDrawListFlags_AntiAliasedLines : 0) | (g.Style.AntiAliasedFill ? ImDrawListFlags_AntiAliasedFill : 0); - window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); - ImRect viewport_rect(GetViewportRect()); - if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) - PushClipRect(parent_window->ClipRect.Min, parent_window->ClipRect.Max, true); - else - PushClipRect(viewport_rect.Min, viewport_rect.Max, true); - - // Draw modal window background (darkens what is behind them) - if ((flags & ImGuiWindowFlags_Modal) != 0 && window == GetFrontMostModalRootWindow()) - window->DrawList->AddRectFilled(viewport_rect.Min, viewport_rect.Max, GetColorU32(ImGuiCol_ModalWindowDarkening, g.ModalWindowDarkeningRatio)); - - // Draw navigation selection/windowing rectangle background - if (g.NavWindowingTarget == window) - { - ImRect bb = window->Rect(); - bb.Expand(g.FontSize); - if (!bb.Contains(viewport_rect)) // Avoid drawing if the window covers all the viewport anyway - window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha * 0.25f), g.Style.WindowRounding); - } - - // Draw window + handle manual resize - const float window_rounding = window->WindowRounding; - const float window_border_size = window->WindowBorderSize; - const bool title_bar_is_highlight = want_focus || (g.NavWindow && window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); - const ImRect title_bar_rect = window->TitleBarRect(); - if (window->Collapsed) - { - // Title bar only - float backup_border_size = style.FrameBorderSize; - g.Style.FrameBorderSize = window->WindowBorderSize; - ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); - RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); - g.Style.FrameBorderSize = backup_border_size; - } - else - { - // Window background - ImU32 bg_col = GetColorU32(GetWindowBgColorIdxFromFlags(flags)); - if (g.NextWindowData.BgAlphaCond != 0) - { - bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(g.NextWindowData.BgAlphaVal) << IM_COL32_A_SHIFT); - g.NextWindowData.BgAlphaCond = 0; - } - window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight()), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Bot); - - // Title bar - ImU32 title_bar_col = GetColorU32(window->Collapsed ? ImGuiCol_TitleBgCollapsed : title_bar_is_highlight ? ImGuiCol_TitleBgActive : - ImGuiCol_TitleBg); - if (!(flags & ImGuiWindowFlags_NoTitleBar)) - window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawCornerFlags_Top); - - // Menu bar - if (flags & ImGuiWindowFlags_MenuBar) - { - ImRect menu_bar_rect = window->MenuBarRect(); - menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. - window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawCornerFlags_Top); - if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) - window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); - } - - // Scrollbars - if (window->ScrollbarX) - Scrollbar(ImGuiLayoutType_Horizontal); - if (window->ScrollbarY) - Scrollbar(ImGuiLayoutType_Vertical); - - // Render resize grips (after their input handling so we don't have a frame of latency) - if (!(flags & ImGuiWindowFlags_NoResize)) - { - for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) - { - const ImGuiResizeGripDef &grip = resize_grip_def[resize_grip_n]; - const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPos); - window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, grip_draw_size) : ImVec2(grip_draw_size, window_border_size))); - window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(grip_draw_size, window_border_size) : ImVec2(window_border_size, grip_draw_size))); - window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); - window->DrawList->PathFillConvex(resize_grip_col[resize_grip_n]); - } - } - - // Borders - if (window_border_size > 0.0f) - window->DrawList->AddRect(window->Pos, window->Pos + window->Size, GetColorU32(ImGuiCol_Border), window_rounding, ImDrawCornerFlags_All, window_border_size); - if (border_held != -1) - { - ImRect border = GetBorderRect(window, border_held, grip_draw_size, 0.0f); - window->DrawList->AddLine(border.Min, border.Max, GetColorU32(ImGuiCol_SeparatorActive), ImMax(1.0f, window_border_size)); - } - if (style.FrameBorderSize > 0 && !(flags & ImGuiWindowFlags_NoTitleBar)) - window->DrawList->AddLine(title_bar_rect.GetBL() + ImVec2(style.WindowBorderSize, -1), title_bar_rect.GetBR() + ImVec2(-style.WindowBorderSize, -1), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); - } - - // Draw navigation selection/windowing rectangle border - if (g.NavWindowingTarget == window) - { - float rounding = ImMax(window->WindowRounding, g.Style.WindowRounding); - ImRect bb = window->Rect(); - bb.Expand(g.FontSize); - if (bb.Contains(viewport_rect)) // If a window fits the entire viewport, adjust its highlight inward - { - bb.Expand(-g.FontSize - 1.0f); - rounding = window->WindowRounding; - } - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), rounding, ~0, 3.0f); - } - - // Store a backup of SizeFull which we will use next frame to decide if we need scrollbars. - window->SizeFullAtLastBegin = window->SizeFull; - - // Update ContentsRegionMax. All the variable it depends on are set above in this function. - window->ContentsRegionRect.Min.x = -window->Scroll.x + window->WindowPadding.x; - window->ContentsRegionRect.Min.y = -window->Scroll.y + window->WindowPadding.y + window->TitleBarHeight() + window->MenuBarHeight(); - window->ContentsRegionRect.Max.x = -window->Scroll.x - window->WindowPadding.x + (window->SizeContentsExplicit.x != 0.0f ? window->SizeContentsExplicit.x : (window->Size.x - window->ScrollbarSizes.x)); - window->ContentsRegionRect.Max.y = -window->Scroll.y - window->WindowPadding.y + (window->SizeContentsExplicit.y != 0.0f ? window->SizeContentsExplicit.y : (window->Size.y - window->ScrollbarSizes.y)); - - // Setup drawing context - // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) - window->DC.IndentX = 0.0f + window->WindowPadding.x - window->Scroll.x; - window->DC.GroupOffsetX = 0.0f; - window->DC.ColumnsOffsetX = 0.0f; - window->DC.CursorStartPos = window->Pos + ImVec2(window->DC.IndentX + window->DC.ColumnsOffsetX, window->TitleBarHeight() + window->MenuBarHeight() + window->WindowPadding.y - window->Scroll.y); - window->DC.CursorPos = window->DC.CursorStartPos; - window->DC.CursorPosPrevLine = window->DC.CursorPos; - window->DC.CursorMaxPos = window->DC.CursorStartPos; - window->DC.CurrentLineHeight = window->DC.PrevLineHeight = 0.0f; - window->DC.CurrentLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; - window->DC.NavHideHighlightOneFrame = false; - window->DC.NavHasScroll = (GetScrollMaxY() > 0.0f); - window->DC.NavLayerActiveMask = window->DC.NavLayerActiveMaskNext; - window->DC.NavLayerActiveMaskNext = 0x00; - window->DC.MenuBarAppending = false; - window->DC.MenuBarOffsetX = ImMax(window->WindowPadding.x, style.ItemSpacing.x); - window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; - window->DC.ChildWindows.resize(0); - window->DC.LayoutType = ImGuiLayoutType_Vertical; - window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; - window->DC.ItemFlags = ImGuiItemFlags_Default_; - window->DC.ItemWidth = window->ItemWidthDefault; - window->DC.TextWrapPos = -1.0f; // disabled - window->DC.ItemFlagsStack.resize(0); - window->DC.ItemWidthStack.resize(0); - window->DC.TextWrapPosStack.resize(0); - window->DC.ColumnsSet = NULL; - window->DC.TreeDepth = 0; - window->DC.TreeDepthMayJumpToParentOnPop = 0x00; - window->DC.StateStorage = &window->StateStorage; - window->DC.GroupStack.resize(0); - window->MenuColumns.Update(3, style.ItemSpacing.x, window_just_activated_by_user); - - if ((flags & ImGuiWindowFlags_ChildWindow) && (window->DC.ItemFlags != parent_window->DC.ItemFlags)) - { - window->DC.ItemFlags = parent_window->DC.ItemFlags; - window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); - } - - if (window->AutoFitFramesX > 0) - window->AutoFitFramesX--; - if (window->AutoFitFramesY > 0) - window->AutoFitFramesY--; - - // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) - if (want_focus) - { - FocusWindow(window); - NavInitWindow(window, false); - } - - // Title bar - if (!(flags & ImGuiWindowFlags_NoTitleBar)) - { - // Close & collapse button are on layer 1 (same as menus) and don't default focus - const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; - window->DC.ItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; - window->DC.NavLayerCurrent++; - window->DC.NavLayerCurrentMask <<= 1; - - // Collapse button - if (!(flags & ImGuiWindowFlags_NoCollapse)) - { - ImGuiID id = window->GetID("#COLLAPSE"); - ImRect bb(window->Pos + style.FramePadding + ImVec2(1, 1), window->Pos + style.FramePadding + ImVec2(g.FontSize, g.FontSize) - ImVec2(1, 1)); - ItemAdd(bb, id); // To allow navigation - if (ButtonBehavior(bb, id, NULL, NULL)) - window->CollapseToggleWanted = true; // Defer collapsing to next frame as we are too far in the Begin() function - RenderNavHighlight(bb, id); - RenderTriangle(window->Pos + style.FramePadding, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); - } - - // Close button - if (p_open != NULL) - { - const float PAD = 2.0f; - const float rad = (window->TitleBarHeight() - PAD * 2.0f) * 0.5f; - if (CloseButton(window->GetID("#CLOSE"), window->Rect().GetTR() + ImVec2(-PAD - rad, PAD + rad), rad)) - *p_open = false; - } - - window->DC.NavLayerCurrent--; - window->DC.NavLayerCurrentMask >>= 1; - window->DC.ItemFlags = item_flags_backup; - - // Title text (FIXME: refactor text alignment facilities along with RenderText helpers) - ImVec2 text_size = CalcTextSize(name, NULL, true); - ImRect text_r = title_bar_rect; - float pad_left = (flags & ImGuiWindowFlags_NoCollapse) == 0 ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; - float pad_right = (p_open != NULL) ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; - if (style.WindowTitleAlign.x > 0.0f) - pad_right = ImLerp(pad_right, pad_left, style.WindowTitleAlign.x); - text_r.Min.x += pad_left; - text_r.Max.x -= pad_right; - ImRect clip_rect = text_r; - clip_rect.Max.x = window->Pos.x + window->Size.x - (p_open ? title_bar_rect.GetHeight() - 3 : style.FramePadding.x); // Match the size of CloseButton() - RenderTextClipped(text_r.Min, text_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_rect); - } - - // Save clipped aabb so we can access it in constant-time in FindHoveredWindow() - window->WindowRectClipped = window->Rect(); - window->WindowRectClipped.ClipWith(window->ClipRect); - - // Pressing CTRL+C while holding on a window copy its content to the clipboard - // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. - // Maybe we can support CTRL+C on every element? - /* - if (g.ActiveId == move_id) - if (g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_C)) - ImGui::LogToClipboard(); - */ - - // Inner rectangle - // We set this up after processing the resize grip so that our clip rectangle doesn't lag by a frame - // Note that if our window is collapsed we will end up with a null clipping rectangle which is the correct behavior. - window->InnerRect.Min.x = title_bar_rect.Min.x + window->WindowBorderSize; - window->InnerRect.Min.y = title_bar_rect.Max.y + window->MenuBarHeight() + (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); - window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->ScrollbarSizes.x - window->WindowBorderSize; - window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->ScrollbarSizes.y - window->WindowBorderSize; - // window->DrawList->AddRect(window->InnerRect.Min, window->InnerRect.Max, IM_COL32_WHITE); - - // After Begin() we fill the last item / hovered data using the title bar data. Make that a standard behavior (to allow usage of context menus on title bar only, etc.). - window->DC.LastItemId = window->MoveId; - window->DC.LastItemStatusFlags = IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; - window->DC.LastItemRect = title_bar_rect; - } - - // Inner clipping rectangle - // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. - const float border_size = window->WindowBorderSize; - ImRect clip_rect; - clip_rect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(0.0f, ImFloor(window->WindowPadding.x * 0.5f - border_size))); - clip_rect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y); - clip_rect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(0.0f, ImFloor(window->WindowPadding.x * 0.5f - border_size))); - clip_rect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y); - PushClipRect(clip_rect.Min, clip_rect.Max, true); - - // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) - if (first_begin_of_the_frame) - window->WriteAccessed = false; - - window->BeginCount++; - g.NextWindowData.SizeConstraintCond = 0; - - // Child window can be out of sight and have "negative" clip windows. - // Mark them as collapsed so commands are skipped earlier (we can't manually collapse because they have no title bar). - if (flags & ImGuiWindowFlags_ChildWindow) - { - IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); - window->Collapsed = parent_window && parent_window->Collapsed; - - if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) - window->Collapsed |= (window->WindowRectClipped.Min.x >= window->WindowRectClipped.Max.x || window->WindowRectClipped.Min.y >= window->WindowRectClipped.Max.y); - - // We also hide the window from rendering because we've already added its border to the command list. - // (we could perform the check earlier in the function but it is simpler at this point) - if (window->Collapsed) - window->Active = false; - } - if (style.Alpha <= 0.0f) - window->Active = false; - - // Return false if we don't intend to display anything to allow user to perform an early out optimization - window->SkipItems = (window->Collapsed || !window->Active) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0; - return !window->SkipItems; -} - -// Old Begin() API with 5 parameters, avoid calling this version directly! Use SetNextWindowSize()/SetNextWindowBgAlpha() + Begin() instead. -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -bool ImGui::Begin(const char *name, bool *p_open, const ImVec2 &size_first_use, float bg_alpha_override, ImGuiWindowFlags flags) +// Layer is locked for the root window, however child windows may use a different viewport (e.g. extruding menu) +static inline void AddRootWindowToDrawData(ImGuiWindow* window) { - // Old API feature: we could pass the initial window size as a parameter. This was misleading because it only had an effect if the window didn't have data in the .ini file. - if (size_first_use.x != 0.0f || size_first_use.y != 0.0f) - ImGui::SetNextWindowSize(size_first_use, ImGuiCond_FirstUseEver); - - // Old API feature: override the window background alpha with a parameter. - if (bg_alpha_override >= 0.0f) - ImGui::SetNextWindowBgAlpha(bg_alpha_override); - - return ImGui::Begin(name, p_open, flags); + AddWindowToDrawData(window, GetWindowDisplayLayer(window)); } -#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS -void ImGui::End() +static void FlattenDrawDataIntoSingleLayer(ImDrawDataBuilder* builder) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - if (window->DC.ColumnsSet != NULL) - EndColumns(); - PopClipRect(); // Inner window clip rectangle - - // Stop logging - if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging - LogFinish(); - - // Pop from window stack - g.CurrentWindowStack.pop_back(); - if (window->Flags & ImGuiWindowFlags_Popup) - g.CurrentPopupStack.pop_back(); - CheckStacksSize(window, false); - SetCurrentWindow(g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back()); -} - -// Vertical scrollbar -// The entire piece of code below is rather confusing because: -// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) -// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar -// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. -void ImGui::Scrollbar(ImGuiLayoutType direction) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - const bool horizontal = (direction == ImGuiLayoutType_Horizontal); - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(horizontal ? "#SCROLLX" : "#SCROLLY"); - - // Render background - bool other_scrollbar = (horizontal ? window->ScrollbarY : window->ScrollbarX); - float other_scrollbar_size_w = other_scrollbar ? style.ScrollbarSize : 0.0f; - const ImRect window_rect = window->Rect(); - const float border_size = window->WindowBorderSize; - ImRect bb = horizontal ? ImRect(window->Pos.x + border_size, window_rect.Max.y - style.ScrollbarSize, window_rect.Max.x - other_scrollbar_size_w - border_size, window_rect.Max.y - border_size) : ImRect(window_rect.Max.x - style.ScrollbarSize, window->Pos.y + border_size, window_rect.Max.x - border_size, window_rect.Max.y - other_scrollbar_size_w - border_size); - if (!horizontal) - bb.Min.y += window->TitleBarHeight() + ((window->Flags & ImGuiWindowFlags_MenuBar) ? window->MenuBarHeight() : 0.0f); - if (bb.GetWidth() <= 0.0f || bb.GetHeight() <= 0.0f) - return; - - int window_rounding_corners; - if (horizontal) - window_rounding_corners = ImDrawCornerFlags_BotLeft | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); - else - window_rounding_corners = (((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) ? ImDrawCornerFlags_TopRight : 0) | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); - window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_ScrollbarBg), window->WindowRounding, window_rounding_corners); - bb.Expand(ImVec2(-ImClamp((float) (int) ((bb.Max.x - bb.Min.x - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp((float) (int) ((bb.Max.y - bb.Min.y - 2.0f) * 0.5f), 0.0f, 3.0f))); - - // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) - float scrollbar_size_v = horizontal ? bb.GetWidth() : bb.GetHeight(); - float scroll_v = horizontal ? window->Scroll.x : window->Scroll.y; - float win_size_avail_v = (horizontal ? window->SizeFull.x : window->SizeFull.y) - other_scrollbar_size_w; - float win_size_contents_v = horizontal ? window->SizeContents.x : window->SizeContents.y; - - // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) - // But we maintain a minimum size in pixel to allow for the user to still aim inside. - IM_ASSERT(ImMax(win_size_contents_v, win_size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. - const float win_size_v = ImMax(ImMax(win_size_contents_v, win_size_avail_v), 1.0f); - const float grab_h_pixels = ImClamp(scrollbar_size_v * (win_size_avail_v / win_size_v), style.GrabMinSize, scrollbar_size_v); - const float grab_h_norm = grab_h_pixels / scrollbar_size_v; - - // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). - bool held = false; - bool hovered = false; - const bool previously_held = (g.ActiveId == id); - ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); - - float scroll_max = ImMax(1.0f, win_size_contents_v - win_size_avail_v); - float scroll_ratio = ImSaturate(scroll_v / scroll_max); - float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; - if (held && grab_h_norm < 1.0f) - { - float scrollbar_pos_v = horizontal ? bb.Min.x : bb.Min.y; - float mouse_pos_v = horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; - float *click_delta_to_grab_center_v = horizontal ? &g.ScrollbarClickDeltaToGrabCenter.x : &g.ScrollbarClickDeltaToGrabCenter.y; - - // Click position in scrollbar normalized space (0.0f->1.0f) - const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); - SetHoveredID(id); - - bool seek_absolute = false; - if (!previously_held) - { - // On initial click calculate the distance between mouse and the center of the grab - if (clicked_v_norm >= grab_v_norm && clicked_v_norm <= grab_v_norm + grab_h_norm) - { - *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; - } - else - { - seek_absolute = true; - *click_delta_to_grab_center_v = 0.0f; - } - } - - // Apply scroll - // It is ok to modify Scroll here because we are being called in Begin() after the calculation of SizeContents and before setting up our starting position - const float scroll_v_norm = ImSaturate((clicked_v_norm - *click_delta_to_grab_center_v - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); - scroll_v = (float) (int) (0.5f + scroll_v_norm * scroll_max); //(win_size_contents_v - win_size_v)); - if (horizontal) - window->Scroll.x = scroll_v; - else - window->Scroll.y = scroll_v; - - // Update values for rendering - scroll_ratio = ImSaturate(scroll_v / scroll_max); - grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; - - // Update distance to grab now that we have seeked and saturated - if (seek_absolute) - *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; - } - - // Render - const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : - ImGuiCol_ScrollbarGrab); - ImRect grab_rect; - if (horizontal) - grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImMin(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, window_rect.Max.x), bb.Max.y); - else - grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImMin(ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels, window_rect.Max.y)); - window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); -} - -void ImGui::BringWindowToFront(ImGuiWindow *window) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *current_front_window = g.Windows.back(); - if (current_front_window == window || current_front_window->RootWindow == window) - return; - for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the front most window - if (g.Windows[i] == window) - { - g.Windows.erase(g.Windows.Data + i); - g.Windows.push_back(window); - break; - } -} - -void ImGui::BringWindowToBack(ImGuiWindow *window) -{ - ImGuiContext &g = *GImGui; - if (g.Windows[0] == window) - return; - for (int i = 0; i < g.Windows.Size; i++) - if (g.Windows[i] == window) - { - memmove(&g.Windows[1], &g.Windows[0], (size_t) i * sizeof(ImGuiWindow *)); - g.Windows[0] = window; - break; - } + int n = builder->Layers[0]->Size; + int full_size = n; + for (int i = 1; i < IM_ARRAYSIZE(builder->Layers); i++) + full_size += builder->Layers[i]->Size; + builder->Layers[0]->resize(full_size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(builder->Layers); layer_n++) + { + ImVector* layer = builder->Layers[layer_n]; + if (layer->empty()) + continue; + memcpy(builder->Layers[0]->Data + n, layer->Data, layer->Size * sizeof(ImDrawList*)); + n += layer->Size; + layer->resize(0); + } } -// Moving window to front of display and set focus (which happens to be back of our sorted list) -void ImGui::FocusWindow(ImGuiWindow *window) +static void InitViewportDrawData(ImGuiViewportP* viewport) { - ImGuiContext &g = *GImGui; - - if (g.NavWindow != window) - { - g.NavWindow = window; - if (window && g.NavDisableMouseHover) - g.NavMousePosDirty = true; - g.NavInitRequest = false; - g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId - g.NavIdIsAlive = false; - g.NavLayer = 0; - } - - // Passing NULL allow to disable keyboard focus - if (!window) - return; - - // Move the root window to the top of the pile - if (window->RootWindow) - window = window->RootWindow; + ImGuiIO& io = ImGui::GetIO(); + ImDrawData* draw_data = &viewport->DrawDataP; - // Steal focus on active widgets - if (window->Flags & ImGuiWindowFlags_Popup) // FIXME: This statement should be unnecessary. Need further testing before removing it.. - if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != window) - ClearActiveID(); + viewport->DrawDataBuilder.Layers[0] = &draw_data->CmdLists; + viewport->DrawDataBuilder.Layers[1] = &viewport->DrawDataBuilder.LayerData1; + viewport->DrawDataBuilder.Layers[0]->resize(0); + viewport->DrawDataBuilder.Layers[1]->resize(0); - // Bring to front - if (!(window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) - BringWindowToFront(window); + draw_data->Valid = true; + draw_data->CmdListsCount = 0; + draw_data->TotalVtxCount = draw_data->TotalIdxCount = 0; + draw_data->DisplayPos = viewport->Pos; + draw_data->DisplaySize = viewport->Size; + draw_data->FramebufferScale = io.DisplayFramebufferScale; + draw_data->OwnerViewport = viewport; } -void ImGui::FocusFrontMostActiveWindow(ImGuiWindow *ignore_window) +// Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. +// - When using this function it is sane to ensure that float are perfectly rounded to integer values, +// so that e.g. (int)(max.x-min.x) in user's render produce correct result. +// - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect(): +// some frequently called functions which to modify both channels and clipping simultaneously tend to use the +// more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds. +// - This is analoguous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack, +// which in the case of ClipRect is not so problematic but tends to be more restrictive for fonts. +void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) { - ImGuiContext &g = *GImGui; - for (int i = g.Windows.Size - 1; i >= 0; i--) - if (g.Windows[i] != ignore_window && g.Windows[i]->WasActive && !(g.Windows[i]->Flags & ImGuiWindowFlags_ChildWindow)) - { - ImGuiWindow *focus_window = NavRestoreLastChildNavWindow(g.Windows[i]); - FocusWindow(focus_window); - return; - } + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); + window->ClipRect = window->DrawList->_ClipRectStack.back(); } -void ImGui::PushItemWidth(float item_width) +void ImGui::PopClipRect() { - ImGuiWindow *window = GetCurrentWindow(); - window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); - window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PopClipRect(); + window->ClipRect = window->DrawList->_ClipRectStack.back(); } -void ImGui::PushMultiItemsWidths(int components, float w_full) +static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) { - ImGuiWindow *window = GetCurrentWindow(); - const ImGuiStyle &style = GImGui->Style; - if (w_full <= 0.0f) - w_full = CalcItemWidth(); - const float w_item_one = ImMax(1.0f, (float) (int) ((w_full - (style.ItemInnerSpacing.x) * (components - 1)) / (float) components)); - const float w_item_last = ImMax(1.0f, (float) (int) (w_full - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); - window->DC.ItemWidthStack.push_back(w_item_last); - for (int i = 0; i < components - 1; i++) - window->DC.ItemWidthStack.push_back(w_item_one); - window->DC.ItemWidth = window->DC.ItemWidthStack.back(); -} + if ((col & IM_COL32_A_MASK) == 0) + return; -void ImGui::PopItemWidth() -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DC.ItemWidthStack.pop_back(); - window->DC.ItemWidth = window->DC.ItemWidthStack.empty() ? window->ItemWidthDefault : window->DC.ItemWidthStack.back(); -} + ImGuiViewportP* viewport = (ImGuiViewportP*)GetMainViewport(); + ImRect viewport_rect = viewport->GetMainRect(); -float ImGui::CalcItemWidth() -{ - ImGuiWindow *window = GetCurrentWindowRead(); - float w = window->DC.ItemWidth; - if (w < 0.0f) - { - // Align to a right-side limit. We include 1 frame padding in the calculation because this is how the width is always used (we add 2 frame padding to it), but we could move that responsibility to the widget as well. - float width_to_right_edge = GetContentRegionAvail().x; - w = ImMax(1.0f, width_to_right_edge + w); - } - w = (float) (int) w; - return w; + // Draw behind window by moving the draw command at the FRONT of the draw list + { + // We've already called AddWindowToDrawData() which called DrawList->ChannelsMerge() on DockNodeHost windows, + // and draw list have been trimmed already, hence the explicit recreation of a draw command if missing. + // FIXME: This is creating complication, might be simpler if we could inject a drawlist in drawdata at a given position and not attempt to manipulate ImDrawCmd order. + ImDrawList* draw_list = window->RootWindow->DrawList; + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // FIXME: Need to stricty ensure ImDrawCmd are not merged (ElemCount==6 checks below will verify that) + draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col); + ImDrawCmd cmd = draw_list->CmdBuffer.back(); + IM_ASSERT(cmd.ElemCount == 6); + draw_list->CmdBuffer.pop_back(); + draw_list->CmdBuffer.push_front(cmd); + draw_list->AddDrawCmd(); // We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command. + draw_list->PopClipRect(); + } } -static ImFont *GetDefaultFont() +ImGuiWindow* ImGui::FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* parent_window) { - ImGuiContext &g = *GImGui; - return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; + ImGuiContext& g = *GImGui; + ImGuiWindow* bottom_most_visible_window = parent_window; + for (int i = FindWindowDisplayIndex(parent_window); i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + if (!IsWindowWithinBeginStackOf(window, parent_window)) + break; + if (IsWindowActiveAndVisible(window) && GetWindowDisplayLayer(window) <= GetWindowDisplayLayer(parent_window)) + bottom_most_visible_window = window; + } + return bottom_most_visible_window; } -void ImGui::SetCurrentFont(ImFont *font) +static void ImGui::RenderDimmedBackgrounds() { - ImGuiContext &g = *GImGui; - IM_ASSERT(font && font->IsLoaded()); // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ? - IM_ASSERT(font->Scale > 0.0f); - g.Font = font; - g.FontBaseSize = g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale; - g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; - - ImFontAtlas *atlas = g.Font->ContainerAtlas; - g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; - g.DrawListSharedData.Font = g.Font; - g.DrawListSharedData.FontSize = g.FontSize; -} + ImGuiContext& g = *GImGui; + ImGuiWindow* modal_window = GetTopMostAndVisiblePopupModal(); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + return; + const bool dim_bg_for_modal = (modal_window != NULL); + const bool dim_bg_for_window_list = (g.NavWindowingTargetAnim != NULL && g.NavWindowingTargetAnim->Active); + if (!dim_bg_for_modal && !dim_bg_for_window_list) + return; -void ImGui::PushFont(ImFont *font) -{ - ImGuiContext &g = *GImGui; - if (!font) - font = GetDefaultFont(); - SetCurrentFont(font); - g.FontStack.push_back(font); - g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); + if (dim_bg_for_modal) + { + // Draw dimming behind modal or a begin stack child, whichever comes first in draw order. + ImGuiWindow* dim_behind_window = FindBottomMostVisibleWindowWithinBeginStack(modal_window); + RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(modal_window->DC.ModalDimBgColor, g.DimBgRatio)); + } + else if (dim_bg_for_window_list) + { + // Draw dimming behind CTRL+Tab target window and behind CTRL+Tab UI window + RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + + // Draw border around CTRL+Tab target window + ImGuiWindow* window = g.NavWindowingTargetAnim; + ImGuiViewport* viewport = GetMainViewport(); + float distance = g.FontSize; + ImRect bb = window->Rect(); + bb.Expand(distance); + if (bb.GetWidth() >= viewport->Size.x && bb.GetHeight() >= viewport->Size.y) + bb.Expand(-distance - 1.0f); // If a window fits the entire viewport, adjust its highlight inward + if (window->DrawList->CmdBuffer.Size == 0) + window->DrawList->AddDrawCmd(); + window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); + window->DrawList->PopClipRect(); + } } -void ImGui::PopFont() +// This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. +void ImGui::EndFrame() { - ImGuiContext &g = *GImGui; - g.CurrentWindow->DrawList->PopTextureID(); - g.FontStack.pop_back(); - SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); -} + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); -void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (enabled) - window->DC.ItemFlags |= option; - else - window->DC.ItemFlags &= ~option; - window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); -} + // Don't process EndFrame() multiple times. + if (g.FrameCountEnded == g.FrameCount) + return; + IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?"); -void ImGui::PopItemFlag() -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DC.ItemFlagsStack.pop_back(); - window->DC.ItemFlags = window->DC.ItemFlagsStack.empty() ? ImGuiItemFlags_Default_ : window->DC.ItemFlagsStack.back(); -} + CallContextHooks(&g, ImGuiContextHookType_EndFramePre); -void ImGui::PushAllowKeyboardFocus(bool allow_keyboard_focus) -{ - PushItemFlag(ImGuiItemFlags_AllowKeyboardFocus, allow_keyboard_focus); -} + ErrorCheckEndFrameSanityChecks(); -void ImGui::PopAllowKeyboardFocus() -{ - PopItemFlag(); -} + // Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) + ImGuiPlatformImeData* ime_data = &g.PlatformImeData; + if (g.PlatformIO.Platform_SetImeDataFn != NULL && memcmp(ime_data, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0) + { + IMGUI_DEBUG_LOG_IO("[io] Calling Platform_SetImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f)\n", ime_data->WantVisible, ime_data->InputPos.x, ime_data->InputPos.y); + ImGuiViewport* viewport = GetMainViewport(); + g.PlatformIO.Platform_SetImeDataFn(&g, viewport, ime_data); + } -void ImGui::PushButtonRepeat(bool repeat) -{ - PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); -} + // Hide implicit/fallback "Debug" window if it hasn't been used + g.WithinFrameScopeWithImplicitWindow = false; + if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) + g.CurrentWindow->Active = false; + End(); -void ImGui::PopButtonRepeat() -{ - PopItemFlag(); -} + // Update navigation: CTRL+Tab, wrap-around requests + NavEndFrame(); -void ImGui::PushTextWrapPos(float wrap_pos_x) -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DC.TextWrapPos = wrap_pos_x; - window->DC.TextWrapPosStack.push_back(wrap_pos_x); -} + // Drag and Drop: Elapse payload (if delivered, or if source stops being submitted) + if (g.DragDropActive) + { + bool is_delivered = g.DragDropPayload.Delivery; + bool is_elapsed = (g.DragDropSourceFrameCount + 1 < g.FrameCount) && ((g.DragDropSourceFlags & ImGuiDragDropFlags_PayloadAutoExpire) || g.DragDropMouseButton == -1 || !IsMouseDown(g.DragDropMouseButton)); + if (is_delivered || is_elapsed) + ClearDragDrop(); + } -void ImGui::PopTextWrapPos() -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DC.TextWrapPosStack.pop_back(); - window->DC.TextWrapPos = window->DC.TextWrapPosStack.empty() ? -1.0f : window->DC.TextWrapPosStack.back(); -} + // Drag and Drop: Fallback for missing source tooltip. This is not ideal but better than nothing. + // If you want to handle source item disappearing: instead of submitting your description tooltip + // in the BeginDragDropSource() block of the dragged item, you can submit them from a safe single spot + // (e.g. end of your item loop, or before EndFrame) by reading payload data. + // In the typical case, the contents of drag tooltip should be possible to infer solely from payload data. + if (g.DragDropActive && g.DragDropSourceFrameCount < g.FrameCount && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + g.DragDropWithinSource = true; + SetTooltip("..."); + g.DragDropWithinSource = false; + } -// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 -void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) -{ - ImGuiContext &g = *GImGui; - ImGuiColMod backup; - backup.Col = idx; - backup.BackupValue = g.Style.Colors[idx]; - g.ColorModifiers.push_back(backup); - g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); -} + // End frame + g.WithinFrameScope = false; + g.FrameCountEnded = g.FrameCount; -void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4 &col) -{ - ImGuiContext &g = *GImGui; - ImGuiColMod backup; - backup.Col = idx; - backup.BackupValue = g.Style.Colors[idx]; - g.ColorModifiers.push_back(backup); - g.Style.Colors[idx] = col; -} + // Initiate moving window + handle left-click and right-click focus + UpdateMouseMovingWindowEndFrame(); -void ImGui::PopStyleColor(int count) -{ - ImGuiContext &g = *GImGui; - while (count > 0) - { - ImGuiColMod &backup = g.ColorModifiers.back(); - g.Style.Colors[backup.Col] = backup.BackupValue; - g.ColorModifiers.pop_back(); - count--; - } -} + // Sort the window list so that all child windows are after their parent + // We cannot do that on FocusWindow() because children may not exist yet + g.WindowsTempSortBuffer.resize(0); + g.WindowsTempSortBuffer.reserve(g.Windows.Size); + for (ImGuiWindow* window : g.Windows) + { + if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it + continue; + AddWindowToSortBuffer(&g.WindowsTempSortBuffer, window); + } -struct ImGuiStyleVarInfo -{ - ImGuiDataType Type; - ImU32 Offset; - void *GetVarPtr(ImGuiStyle *style) const - { - return (void *) ((unsigned char *) style + Offset); - } -}; + // This usually assert if there is a mismatch between the ImGuiWindowFlags_ChildWindow / ParentWindow values and DC.ChildWindows[] in parents, aka we've done something wrong. + IM_ASSERT(g.Windows.Size == g.WindowsTempSortBuffer.Size); + g.Windows.swap(g.WindowsTempSortBuffer); + g.IO.MetricsActiveWindows = g.WindowsActiveCount; -static const ImGuiStyleVarInfo GStyleVarInfo[] = - { - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, Alpha)}, // ImGuiStyleVar_Alpha - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, WindowPadding)}, // ImGuiStyleVar_WindowPadding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, WindowRounding)}, // ImGuiStyleVar_WindowRounding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, WindowBorderSize)}, // ImGuiStyleVar_WindowBorderSize - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, WindowMinSize)}, // ImGuiStyleVar_WindowMinSize - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, WindowTitleAlign)}, // ImGuiStyleVar_WindowTitleAlign - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, ChildRounding)}, // ImGuiStyleVar_ChildRounding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, ChildBorderSize)}, // ImGuiStyleVar_ChildBorderSize - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, PopupRounding)}, // ImGuiStyleVar_PopupRounding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, PopupBorderSize)}, // ImGuiStyleVar_PopupBorderSize - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, FramePadding)}, // ImGuiStyleVar_FramePadding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, FrameRounding)}, // ImGuiStyleVar_FrameRounding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, FrameBorderSize)}, // ImGuiStyleVar_FrameBorderSize - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, ItemSpacing)}, // ImGuiStyleVar_ItemSpacing - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing)}, // ImGuiStyleVar_ItemInnerSpacing - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, IndentSpacing)}, // ImGuiStyleVar_IndentSpacing - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, ScrollbarSize)}, // ImGuiStyleVar_ScrollbarSize - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, ScrollbarRounding)}, // ImGuiStyleVar_ScrollbarRounding - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, GrabMinSize)}, // ImGuiStyleVar_GrabMinSize - {ImGuiDataType_Float, (ImU32) IM_OFFSETOF(ImGuiStyle, GrabRounding)}, // ImGuiStyleVar_GrabRounding - {ImGuiDataType_Float2, (ImU32) IM_OFFSETOF(ImGuiStyle, ButtonTextAlign)}, // ImGuiStyleVar_ButtonTextAlign -}; + // Unlock font atlas + g.IO.Fonts->Locked = false; -static const ImGuiStyleVarInfo *GetStyleVarInfo(ImGuiStyleVar idx) -{ - IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_Count_); - IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_Count_); - return &GStyleVarInfo[idx]; -} + // Clear Input data for next frame + g.IO.MousePosPrev = g.IO.MousePos; + g.IO.AppFocusLost = false; + g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; + g.IO.InputQueueCharacters.resize(0); -void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) -{ - const ImGuiStyleVarInfo *var_info = GetStyleVarInfo(idx); - if (var_info->Type == ImGuiDataType_Float) - { - ImGuiContext &g = *GImGui; - float *pvar = (float *) var_info->GetVarPtr(&g.Style); - g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); - *pvar = val; - return; - } - IM_ASSERT(0); // Called function with wrong-type? Variable is not a float. + CallContextHooks(&g, ImGuiContextHookType_EndFramePost); } -void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2 &val) +// Prepare the data for rendering so you can call GetDrawData() +// (As with anything within the ImGui:: namspace this doesn't touch your GPU or graphics API at all: +// it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend) +void ImGui::Render() { - const ImGuiStyleVarInfo *var_info = GetStyleVarInfo(idx); - if (var_info->Type == ImGuiDataType_Float2) - { - ImGuiContext &g = *GImGui; - ImVec2 *pvar = (ImVec2 *) var_info->GetVarPtr(&g.Style); - g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); - *pvar = val; - return; - } - IM_ASSERT(0); // Called function with wrong-type? Variable is not a ImVec2. -} + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); -void ImGui::PopStyleVar(int count) -{ - ImGuiContext &g = *GImGui; - while (count > 0) - { - ImGuiStyleMod &backup = g.StyleModifiers.back(); - const ImGuiStyleVarInfo *info = GetStyleVarInfo(backup.VarIdx); - if (info->Type == ImGuiDataType_Float) - (*(float *) info->GetVarPtr(&g.Style)) = backup.BackupFloat[0]; - else if (info->Type == ImGuiDataType_Float2) - (*(ImVec2 *) info->GetVarPtr(&g.Style)) = ImVec2(backup.BackupFloat[0], backup.BackupFloat[1]); - else if (info->Type == ImGuiDataType_Int) - (*(int *) info->GetVarPtr(&g.Style)) = backup.BackupInt[0]; - g.StyleModifiers.pop_back(); - count--; - } -} - -const char *ImGui::GetStyleColorName(ImGuiCol idx) -{ - // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; - switch (idx) - { - case ImGuiCol_Text: - return "Text"; - case ImGuiCol_TextDisabled: - return "TextDisabled"; - case ImGuiCol_WindowBg: - return "WindowBg"; - case ImGuiCol_ChildBg: - return "ChildBg"; - case ImGuiCol_PopupBg: - return "PopupBg"; - case ImGuiCol_Border: - return "Border"; - case ImGuiCol_BorderShadow: - return "BorderShadow"; - case ImGuiCol_FrameBg: - return "FrameBg"; - case ImGuiCol_FrameBgHovered: - return "FrameBgHovered"; - case ImGuiCol_FrameBgActive: - return "FrameBgActive"; - case ImGuiCol_TitleBg: - return "TitleBg"; - case ImGuiCol_TitleBgActive: - return "TitleBgActive"; - case ImGuiCol_TitleBgCollapsed: - return "TitleBgCollapsed"; - case ImGuiCol_MenuBarBg: - return "MenuBarBg"; - case ImGuiCol_ScrollbarBg: - return "ScrollbarBg"; - case ImGuiCol_ScrollbarGrab: - return "ScrollbarGrab"; - case ImGuiCol_ScrollbarGrabHovered: - return "ScrollbarGrabHovered"; - case ImGuiCol_ScrollbarGrabActive: - return "ScrollbarGrabActive"; - case ImGuiCol_CheckMark: - return "CheckMark"; - case ImGuiCol_SliderGrab: - return "SliderGrab"; - case ImGuiCol_SliderGrabActive: - return "SliderGrabActive"; - case ImGuiCol_Button: - return "Button"; - case ImGuiCol_ButtonHovered: - return "ButtonHovered"; - case ImGuiCol_ButtonActive: - return "ButtonActive"; - case ImGuiCol_Header: - return "Header"; - case ImGuiCol_HeaderHovered: - return "HeaderHovered"; - case ImGuiCol_HeaderActive: - return "HeaderActive"; - case ImGuiCol_Separator: - return "Separator"; - case ImGuiCol_SeparatorHovered: - return "SeparatorHovered"; - case ImGuiCol_SeparatorActive: - return "SeparatorActive"; - case ImGuiCol_ResizeGrip: - return "ResizeGrip"; - case ImGuiCol_ResizeGripHovered: - return "ResizeGripHovered"; - case ImGuiCol_ResizeGripActive: - return "ResizeGripActive"; - case ImGuiCol_CloseButton: - return "CloseButton"; - case ImGuiCol_CloseButtonHovered: - return "CloseButtonHovered"; - case ImGuiCol_CloseButtonActive: - return "CloseButtonActive"; - case ImGuiCol_PlotLines: - return "PlotLines"; - case ImGuiCol_PlotLinesHovered: - return "PlotLinesHovered"; - case ImGuiCol_PlotHistogram: - return "PlotHistogram"; - case ImGuiCol_PlotHistogramHovered: - return "PlotHistogramHovered"; - case ImGuiCol_TextSelectedBg: - return "TextSelectedBg"; - case ImGuiCol_ModalWindowDarkening: - return "ModalWindowDarkening"; - case ImGuiCol_DragDropTarget: - return "DragDropTarget"; - case ImGuiCol_NavHighlight: - return "NavHighlight"; - case ImGuiCol_NavWindowingHighlight: - return "NavWindowingHighlight"; - } - IM_ASSERT(0); - return "Unknown"; -} - -bool ImGui::IsWindowChildOf(ImGuiWindow *window, ImGuiWindow *potential_parent) -{ - if (window->RootWindow == potential_parent) - return true; - while (window != NULL) - { - if (window == potential_parent) - return true; - window = window->ParentWindow; - } - return false; -} + if (g.FrameCountEnded != g.FrameCount) + EndFrame(); + if (g.FrameCountRendered == g.FrameCount) + return; + g.FrameCountRendered = g.FrameCount; -bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) -{ - IM_ASSERT((flags & ImGuiHoveredFlags_AllowWhenOverlapped) == 0); // Flags not supported by this function - ImGuiContext &g = *GImGui; - - if (flags & ImGuiHoveredFlags_AnyWindow) - { - if (g.HoveredWindow == NULL) - return false; - } - else - { - switch (flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) - { - case ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows: - if (g.HoveredRootWindow != g.CurrentWindow->RootWindow) - return false; - break; - case ImGuiHoveredFlags_RootWindow: - if (g.HoveredWindow != g.CurrentWindow->RootWindow) - return false; - break; - case ImGuiHoveredFlags_ChildWindows: - if (g.HoveredWindow == NULL || !IsWindowChildOf(g.HoveredWindow, g.CurrentWindow)) - return false; - break; - default: - if (g.HoveredWindow != g.CurrentWindow) - return false; - break; - } - } - - if (!IsWindowContentHoverable(g.HoveredRootWindow, flags)) - return false; - if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != g.HoveredWindow->MoveId) - return false; - return true; -} + g.IO.MetricsRenderWindows = 0; + CallContextHooks(&g, ImGuiContextHookType_RenderPre); -bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) -{ - ImGuiContext &g = *GImGui; - IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() + // Add background ImDrawList (for each active viewport) + for (ImGuiViewportP* viewport : g.Viewports) + { + InitViewportDrawData(viewport); + if (viewport->BgFgDrawLists[0] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); + } - if (flags & ImGuiFocusedFlags_AnyWindow) - return g.NavWindow != NULL; + // Draw modal/window whitening backgrounds + RenderDimmedBackgrounds(); - switch (flags & (ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows)) - { - case ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows: - return g.NavWindow && g.NavWindow->RootWindow == g.CurrentWindow->RootWindow; - case ImGuiFocusedFlags_RootWindow: - return g.NavWindow == g.CurrentWindow->RootWindow; - case ImGuiFocusedFlags_ChildWindows: - return g.NavWindow && IsWindowChildOf(g.NavWindow, g.CurrentWindow); - default: - return g.NavWindow == g.CurrentWindow; - } -} + // Add ImDrawList to render + ImGuiWindow* windows_to_render_top_most[2]; + windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindow : NULL; + windows_to_render_top_most[1] = (g.NavWindowingTarget ? g.NavWindowingListWindow : NULL); + for (ImGuiWindow* window : g.Windows) + { + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + if (IsWindowActiveAndVisible(window) && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0 && window != windows_to_render_top_most[0] && window != windows_to_render_top_most[1]) + AddRootWindowToDrawData(window); + } + for (int n = 0; n < IM_ARRAYSIZE(windows_to_render_top_most); n++) + if (windows_to_render_top_most[n] && IsWindowActiveAndVisible(windows_to_render_top_most[n])) // NavWindowingTarget is always temporarily displayed as the top-most window + AddRootWindowToDrawData(windows_to_render_top_most[n]); -// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) -bool ImGui::IsWindowNavFocusable(ImGuiWindow *window) -{ - ImGuiContext &g = *GImGui; - return window->Active && window == window->RootWindowForTabbing && (!(window->Flags & ImGuiWindowFlags_NoNavFocus) || window == g.NavWindow); -} + // Draw software mouse cursor if requested by io.MouseDrawCursor flag + if (g.IO.MouseDrawCursor && g.MouseCursor != ImGuiMouseCursor_None) + RenderMouseCursor(g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); -float ImGui::GetWindowWidth() -{ - ImGuiWindow *window = GImGui->CurrentWindow; - return window->Size.x; -} + // Setup ImDrawData structures for end-user + g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = 0; + for (ImGuiViewportP* viewport : g.Viewports) + { + FlattenDrawDataIntoSingleLayer(&viewport->DrawDataBuilder); -float ImGui::GetWindowHeight() -{ - ImGuiWindow *window = GImGui->CurrentWindow; - return window->Size.y; -} + // Add foreground ImDrawList (for each active viewport) + if (viewport->BgFgDrawLists[1] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); -ImVec2 ImGui::GetWindowPos() -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - return window->Pos; -} + // We call _PopUnusedDrawCmd() last thing, as RenderDimmedBackgrounds() rely on a valid command being there (especially in docking branch). + ImDrawData* draw_data = &viewport->DrawDataP; + IM_ASSERT(draw_data->CmdLists.Size == draw_data->CmdListsCount); + for (ImDrawList* draw_list : draw_data->CmdLists) + draw_list->_PopUnusedDrawCmd(); -static void SetWindowScrollX(ImGuiWindow *window, float new_scroll_x) -{ - window->DC.CursorMaxPos.x += window->Scroll.x; // SizeContents is generally computed based on CursorMaxPos which is affected by scroll position, so we need to apply our change to it. - window->Scroll.x = new_scroll_x; - window->DC.CursorMaxPos.x -= window->Scroll.x; -} + g.IO.MetricsRenderVertices += draw_data->TotalVtxCount; + g.IO.MetricsRenderIndices += draw_data->TotalIdxCount; + } -static void SetWindowScrollY(ImGuiWindow *window, float new_scroll_y) -{ - window->DC.CursorMaxPos.y += window->Scroll.y; // SizeContents is generally computed based on CursorMaxPos which is affected by scroll position, so we need to apply our change to it. - window->Scroll.y = new_scroll_y; - window->DC.CursorMaxPos.y -= window->Scroll.y; + CallContextHooks(&g, ImGuiContextHookType_RenderPost); } -static void SetWindowPos(ImGuiWindow *window, const ImVec2 &pos, ImGuiCond cond) +// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. +// CalcTextSize("") should return ImVec2(0.0f, g.FontSize) +ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) { - // Test condition (NB: bit 0 is always true) and clear flags for next time - if (cond && (window->SetWindowPosAllowFlags & cond) == 0) - return; - window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); + ImGuiContext& g = *GImGui; - // Set - const ImVec2 old_pos = window->Pos; - window->PosFloat = pos; - window->Pos = ImFloor(pos); - window->DC.CursorPos += (window->Pos - old_pos); // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor - window->DC.CursorMaxPos += (window->Pos - old_pos); // And more importantly we need to adjust this so size calculation doesn't get affected. -} + const char* text_display_end; + if (hide_text_after_double_hash) + text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string + else + text_display_end = text_end; -void ImGui::SetWindowPos(const ImVec2 &pos, ImGuiCond cond) -{ - ImGuiWindow *window = GetCurrentWindowRead(); - SetWindowPos(window, pos, cond); -} + ImFont* font = g.Font; + const float font_size = g.FontSize; + if (text == text_display_end) + return ImVec2(0.0f, font_size); + ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); -void ImGui::SetWindowPos(const char *name, const ImVec2 &pos, ImGuiCond cond) -{ - if (ImGuiWindow *window = FindWindowByName(name)) - SetWindowPos(window, pos, cond); -} + // Round + // FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out. + // FIXME: Investigate using ceilf or e.g. + // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c + // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html + text_size.x = IM_TRUNC(text_size.x + 0.99999f); -ImVec2 ImGui::GetWindowSize() -{ - ImGuiWindow *window = GetCurrentWindowRead(); - return window->Size; + return text_size; } -static void SetWindowSize(ImGuiWindow *window, const ImVec2 &size, ImGuiCond cond) -{ - // Test condition (NB: bit 0 is always true) and clear flags for next time - if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) - return; - window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); +// Find window given position, search front-to-back +// - Typically write output back to g.HoveredWindow and g.HoveredWindowUnderMovingWindow. +// - FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programmatically +// with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is +// called, aka before the next Begin(). Moving window isn't affected. +// - The 'find_first_and_in_any_viewport = true' mode is only used by TestEngine. It is simpler to maintain here. +void ImGui::FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* hovered_window = NULL; + ImGuiWindow* hovered_window_under_moving_window = NULL; + + if (find_first_and_in_any_viewport == false && g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) + hovered_window = g.MovingWindow; + + ImVec2 padding_regular = g.Style.TouchExtraPadding; + ImVec2 padding_for_resize = g.IO.ConfigWindowsResizeFromEdges ? g.WindowsHoverPadding : padding_regular; + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (!window->Active || window->Hidden) + continue; + if (window->Flags & ImGuiWindowFlags_NoMouseInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImVec2 hit_padding = (window->Flags & (ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) ? padding_regular : padding_for_resize; + if (!window->OuterRectClipped.ContainsWithPad(pos, hit_padding)) + continue; + + // Support for one rectangular hole in any given window + // FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512) + if (window->HitTestHoleSize.x != 0) + { + ImVec2 hole_pos(window->Pos.x + (float)window->HitTestHoleOffset.x, window->Pos.y + (float)window->HitTestHoleOffset.y); + ImVec2 hole_size((float)window->HitTestHoleSize.x, (float)window->HitTestHoleSize.y); + if (ImRect(hole_pos, hole_pos + hole_size).Contains(pos)) + continue; + } + + if (find_first_and_in_any_viewport) + { + hovered_window = window; + break; + } + else + { + if (hovered_window == NULL) + hovered_window = window; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (hovered_window_under_moving_window == NULL && (!g.MovingWindow || window->RootWindow != g.MovingWindow->RootWindow)) + hovered_window_under_moving_window = window; + if (hovered_window && hovered_window_under_moving_window) + break; + } + } - // Set - if (size.x > 0.0f) - { - window->AutoFitFramesX = 0; - window->SizeFull.x = size.x; - } - else - { - window->AutoFitFramesX = 2; - window->AutoFitOnlyGrows = false; - } - if (size.y > 0.0f) - { - window->AutoFitFramesY = 0; - window->SizeFull.y = size.y; - } - else - { - window->AutoFitFramesY = 2; - window->AutoFitOnlyGrows = false; - } + *out_hovered_window = hovered_window; + if (out_hovered_window_under_moving_window != NULL) + *out_hovered_window_under_moving_window = hovered_window_under_moving_window; } -void ImGui::SetWindowSize(const ImVec2 &size, ImGuiCond cond) +bool ImGui::IsItemActive() { - SetWindowSize(GImGui->CurrentWindow, size, cond); + ImGuiContext& g = *GImGui; + if (g.ActiveId) + return g.ActiveId == g.LastItemData.ID; + return false; } -void ImGui::SetWindowSize(const char *name, const ImVec2 &size, ImGuiCond cond) +bool ImGui::IsItemActivated() { - if (ImGuiWindow *window = FindWindowByName(name)) - SetWindowSize(window, size, cond); + ImGuiContext& g = *GImGui; + if (g.ActiveId) + if (g.ActiveId == g.LastItemData.ID && g.ActiveIdPreviousFrame != g.LastItemData.ID) + return true; + return false; } -static void SetWindowCollapsed(ImGuiWindow *window, bool collapsed, ImGuiCond cond) +bool ImGui::IsItemDeactivated() { - // Test condition (NB: bit 0 is always true) and clear flags for next time - if (cond && (window->SetWindowCollapsedAllowFlags & cond) == 0) - return; - window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - - // Set - window->Collapsed = collapsed; + ImGuiContext& g = *GImGui; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDeactivated) + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Deactivated) != 0; + return (g.ActiveIdPreviousFrame == g.LastItemData.ID && g.ActiveIdPreviousFrame != 0 && g.ActiveId != g.LastItemData.ID); } -void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) +bool ImGui::IsItemDeactivatedAfterEdit() { - SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); + ImGuiContext& g = *GImGui; + return IsItemDeactivated() && (g.ActiveIdPreviousFrameHasBeenEditedBefore || (g.ActiveId == 0 && g.ActiveIdHasBeenEditedBefore)); } -bool ImGui::IsWindowCollapsed() +// == GetItemID() == GetFocusID() +bool ImGui::IsItemFocused() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->Collapsed; + ImGuiContext& g = *GImGui; + if (g.NavId != g.LastItemData.ID || g.NavId == 0) + return false; + return true; } -bool ImGui::IsWindowAppearing() +// Important: this can be useful but it is NOT equivalent to the behavior of e.g.Button()! +// Most widgets have specific reactions based on mouse-up/down state, mouse position etc. +bool ImGui::IsItemClicked(ImGuiMouseButton mouse_button) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->Appearing; + return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); } -void ImGui::SetWindowCollapsed(const char *name, bool collapsed, ImGuiCond cond) +bool ImGui::IsItemToggledOpen() { - if (ImGuiWindow *window = FindWindowByName(name)) - SetWindowCollapsed(window, collapsed, cond); + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; } -void ImGui::SetWindowFocus() +// Call after a Selectable() or TreeNode() involved in multi-selection. +// Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose. +// This is only meant to be called inside a BeginMultiSelect()/EndMultiSelect() block. +// (Outside of multi-select, it would be misleading/ambiguous to report this signal, as widgets +// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.) +bool ImGui::IsItemToggledSelection() { - FocusWindow(GImGui->CurrentWindow); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentMultiSelect != NULL); // Can only be used inside a BeginMultiSelect()/EndMultiSelect() + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; } -void ImGui::SetWindowFocus(const char *name) +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. +bool ImGui::IsAnyItemHovered() { - if (name) - { - if (ImGuiWindow *window = FindWindowByName(name)) - FocusWindow(window); - } - else - { - FocusWindow(NULL); - } + ImGuiContext& g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; } -void ImGui::SetNextWindowPos(const ImVec2 &pos, ImGuiCond cond, const ImVec2 &pivot) +bool ImGui::IsAnyItemActive() { - ImGuiContext &g = *GImGui; - g.NextWindowData.PosVal = pos; - g.NextWindowData.PosPivotVal = pivot; - g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; + ImGuiContext& g = *GImGui; + return g.ActiveId != 0; } -void ImGui::SetNextWindowSize(const ImVec2 &size, ImGuiCond cond) +bool ImGui::IsAnyItemFocused() { - ImGuiContext &g = *GImGui; - g.NextWindowData.SizeVal = size; - g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; + ImGuiContext& g = *GImGui; + return g.NavId != 0 && !g.NavDisableHighlight; } -void ImGui::SetNextWindowSizeConstraints(const ImVec2 &size_min, const ImVec2 &size_max, ImGuiSizeCallback custom_callback, void *custom_callback_user_data) +bool ImGui::IsItemVisible() { - ImGuiContext &g = *GImGui; - g.NextWindowData.SizeConstraintCond = ImGuiCond_Always; - g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); - g.NextWindowData.SizeCallback = custom_callback; - g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) != 0; } -void ImGui::SetNextWindowContentSize(const ImVec2 &size) +bool ImGui::IsItemEdited() { - ImGuiContext &g = *GImGui; - g.NextWindowData.ContentSizeVal = size; // In Begin() we will add the size of window decorations (title bar, menu etc.) to that to form a SizeContents value. - g.NextWindowData.ContentSizeCond = ImGuiCond_Always; + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Edited) != 0; } -void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) +// Allow next item to be overlapped by subsequent items. +// This works by requiring HoveredId to match for two subsequent frames, +// so if a following items overwrite it our interactions will naturally be disabled. +void ImGui::SetNextItemAllowOverlap() { - ImGuiContext &g = *GImGui; - g.NextWindowData.CollapsedVal = collapsed; - g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_AllowOverlap; } -void ImGui::SetNextWindowFocus() +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. +// FIXME-LEGACY: Use SetNextItemAllowOverlap() *before* your item instead. +void ImGui::SetItemAllowOverlap() { - ImGuiContext &g = *GImGui; - g.NextWindowData.FocusCond = ImGuiCond_Always; // Using a Cond member for consistency (may transition all of them to single flag set for fast Clear() op) + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (g.HoveredId == id) + g.HoveredIdAllowOverlap = true; + if (g.ActiveId == id) // Before we made this obsolete, most calls to SetItemAllowOverlap() used to avoid this path by testing g.ActiveId != id. + g.ActiveIdAllowOverlap = true; } +#endif -void ImGui::SetNextWindowBgAlpha(float alpha) +// This is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations. +// FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version if needed? +void ImGui::SetActiveIdUsingAllKeyboardKeys() { - ImGuiContext &g = *GImGui; - g.NextWindowData.BgAlphaVal = alpha; - g.NextWindowData.BgAlphaCond = ImGuiCond_Always; // Using a Cond member for consistency (may transition all of them to single flag set for fast Clear() op) + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId != 0); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_COUNT) - 1; + g.ActiveIdUsingAllKeyboardKeys = true; + NavMoveRequestCancel(); } -// In window space (not screen space!) -ImVec2 ImGui::GetContentRegionMax() +ImGuiID ImGui::GetItemID() { - ImGuiWindow *window = GetCurrentWindowRead(); - ImVec2 mx = window->ContentsRegionRect.Max; - if (window->DC.ColumnsSet) - mx.x = GetColumnOffset(window->DC.ColumnsSet->Current + 1) - window->WindowPadding.x; - return mx; + ImGuiContext& g = *GImGui; + return g.LastItemData.ID; } -ImVec2 ImGui::GetContentRegionAvail() +ImVec2 ImGui::GetItemRectMin() { - ImGuiWindow *window = GetCurrentWindowRead(); - return GetContentRegionMax() - (window->DC.CursorPos - window->Pos); + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Min; } -float ImGui::GetContentRegionAvailWidth() +ImVec2 ImGui::GetItemRectMax() { - return GetContentRegionAvail().x; + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Max; } -// In window space (not screen space!) -ImVec2 ImGui::GetWindowContentRegionMin() +ImVec2 ImGui::GetItemRectSize() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Min; + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.GetSize(); } -ImVec2 ImGui::GetWindowContentRegionMax() +// Prior to v1.90 2023/10/16, the BeginChild() function took a 'bool border = false' parameter instead of 'ImGuiChildFlags child_flags = 0'. +// ImGuiChildFlags_Borders is defined as always == 1 in order to allow old code passing 'true'. Read comments in imgui.h for details! +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Max; + ImGuiID id = GetCurrentWindow()->GetID(str_id); + return BeginChildEx(str_id, id, size_arg, child_flags, window_flags); } -float ImGui::GetWindowContentRegionWidth() +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Max.x - window->ContentsRegionRect.Min.x; + return BeginChildEx(NULL, id, size_arg, child_flags, window_flags); } -float ImGui::GetTextLineHeight() +bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { - ImGuiContext &g = *GImGui; - return g.FontSize; -} + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + IM_ASSERT(id != 0); -float ImGui::GetTextLineHeightWithSpacing() -{ - ImGuiContext &g = *GImGui; - return g.FontSize + g.Style.ItemSpacing.y; -} + // Sanity check as it is likely that some user will accidentally pass ImGuiWindowFlags into the ImGuiChildFlags argument. + const ImGuiChildFlags ImGuiChildFlags_SupportedMask_ = ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_FrameStyle | ImGuiChildFlags_NavFlattened; + IM_UNUSED(ImGuiChildFlags_SupportedMask_); + IM_ASSERT((child_flags & ~ImGuiChildFlags_SupportedMask_) == 0 && "Illegal ImGuiChildFlags value. Did you pass ImGuiWindowFlags values instead of ImGuiChildFlags?"); + IM_ASSERT((window_flags & ImGuiWindowFlags_AlwaysAutoResize) == 0 && "Cannot specify ImGuiWindowFlags_AlwaysAutoResize for BeginChild(). Use ImGuiChildFlags_AlwaysAutoResize!"); + if (child_flags & ImGuiChildFlags_AlwaysAutoResize) + { + IM_ASSERT((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0 && "Cannot use ImGuiChildFlags_ResizeX or ImGuiChildFlags_ResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + IM_ASSERT((child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY)) != 0 && "Must use ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window_flags & ImGuiWindowFlags_AlwaysUseWindowPadding) + child_flags |= ImGuiChildFlags_AlwaysUseWindowPadding; + if (window_flags & ImGuiWindowFlags_NavFlattened) + child_flags |= ImGuiChildFlags_NavFlattened; +#endif + if (child_flags & ImGuiChildFlags_AutoResizeX) + child_flags &= ~ImGuiChildFlags_ResizeX; + if (child_flags & ImGuiChildFlags_AutoResizeY) + child_flags &= ~ImGuiChildFlags_ResizeY; + + // Set window flags + window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar; + window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize)) + window_flags |= ImGuiWindowFlags_AlwaysAutoResize; + if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0) + window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + // Special framed style + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PushStyleColor(ImGuiCol_ChildBg, g.Style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, g.Style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, g.Style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.FramePadding); + child_flags |= ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding; + window_flags |= ImGuiWindowFlags_NoMove; + } -float ImGui::GetFrameHeight() -{ - ImGuiContext &g = *GImGui; - return g.FontSize + g.Style.FramePadding.y * 2.0f; -} + // Forward child flags + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasChildFlags; + g.NextWindowData.ChildFlags = child_flags; + + // Forward size + // Important: Begin() has special processing to switch condition to ImGuiCond_FirstUseEver for a given axis when ImGuiChildFlags_ResizeXXX is set. + // (the alternative would to store conditional flags per axis, which is possible but more code) + const ImVec2 size_avail = GetContentRegionAvail(); + const ImVec2 size_default((child_flags & ImGuiChildFlags_AutoResizeX) ? 0.0f : size_avail.x, (child_flags & ImGuiChildFlags_AutoResizeY) ? 0.0f : size_avail.y); + const ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); + SetNextWindowSize(size); + + // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. + // FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround. + // e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it. + const char* temp_window_name; + /*if (name && parent_window->IDStack.back() == parent_window->ID) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s", parent_window->Name, name); // May omit ID if in root of ID stack + else*/ + if (name) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s_%08X", parent_window->Name, name, id); + else + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%08X", parent_window->Name, id); + + // Set style + const float backup_border_size = g.Style.ChildBorderSize; + if ((child_flags & ImGuiChildFlags_Borders) == 0) + g.Style.ChildBorderSize = 0.0f; + + // Begin into window + const bool ret = Begin(temp_window_name, NULL, window_flags); + + // Restore style + g.Style.ChildBorderSize = backup_border_size; + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PopStyleVar(3); + PopStyleColor(); + } -float ImGui::GetFrameHeightWithSpacing() -{ - ImGuiContext &g = *GImGui; - return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; -} + ImGuiWindow* child_window = g.CurrentWindow; + child_window->ChildId = id; -ImDrawList *ImGui::GetWindowDrawList() -{ - ImGuiWindow *window = GetCurrentWindow(); - return window->DrawList; -} + // Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually. + // While this is not really documented/defined, it seems that the expected thing to do. + if (child_window->BeginCount == 1) + parent_window->DC.CursorPos = child_window->Pos; -ImFont *ImGui::GetFont() -{ - return GImGui->Font; + // Process navigation-in immediately so NavInit can run on first frame + // Can enter a child if (A) it has navigable items or (B) it can be scrolled. + const ImGuiID temp_id_for_activation = ImHashStr("##Child", 0, id); + if (g.ActiveId == temp_id_for_activation) + ClearActiveID(); + if (g.NavActivateId == id && !(child_flags & ImGuiChildFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY)) + { + FocusWindow(child_window); + NavInitWindow(child_window, false); + SetActiveID(temp_id_for_activation, child_window); // Steal ActiveId with another arbitrary id so that key-press won't activate child item + g.ActiveIdSource = g.NavInputSource; + } + return ret; } -float ImGui::GetFontSize() +void ImGui::EndChild() { - return GImGui->FontSize; -} + ImGuiContext& g = *GImGui; + ImGuiWindow* child_window = g.CurrentWindow; -ImVec2 ImGui::GetFontTexUvWhitePixel() -{ - return GImGui->DrawListSharedData.TexUvWhitePixel; -} + IM_ASSERT(g.WithinEndChild == false); + IM_ASSERT(child_window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls -void ImGui::SetWindowFontScale(float scale) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - window->FontWindowScale = scale; - g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.WithinEndChild = true; + ImVec2 child_size = child_window->Size; + End(); + if (child_window->BeginCount == 1) + { + ImGuiWindow* parent_window = g.CurrentWindow; + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + child_size); + ItemSize(child_size); + const bool nav_flattened = (child_window->ChildFlags & ImGuiChildFlags_NavFlattened) != 0; + if ((child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY) && !nav_flattened) + { + ItemAdd(bb, child_window->ChildId); + RenderNavHighlight(bb, child_window->ChildId); + + // When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying) + if (child_window->DC.NavLayersActiveMask == 0 && child_window == g.NavWindow) + RenderNavHighlight(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavHighlightFlags_Compact); + } + else + { + // Not navigable into + // - This is a bit of a fringe use case, mostly useful for undecorated, non-scrolling contents childs, or empty childs. + // - We could later decide to not apply this path if ImGuiChildFlags_FrameStyle or ImGuiChildFlags_Borders is set. + ItemAdd(bb, child_window->ChildId, NULL, ImGuiItemFlags_NoNav); + + // But when flattened we directly reach items, adjust active layer mask accordingly + if (nav_flattened) + parent_window->DC.NavLayersActiveMaskNext |= child_window->DC.NavLayersActiveMaskNext; + } + if (g.HoveredWindow == child_window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + } + g.WithinEndChild = false; + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return } -// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. -// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. -ImVec2 ImGui::GetCursorPos() +static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.CursorPos - window->Pos + window->Scroll; + window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); + window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); + window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); } -float ImGui::GetCursorPosX() +ImGuiWindow* ImGui::FindWindowByID(ImGuiID id) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; + ImGuiContext& g = *GImGui; + return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); } -float ImGui::GetCursorPosY() +ImGuiWindow* ImGui::FindWindowByName(const char* name) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; + ImGuiID id = ImHashStr(name); + return FindWindowByID(id); } -void ImGui::SetCursorPos(const ImVec2 &local_pos) +static void ApplyWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) { - ImGuiWindow *window = GetCurrentWindow(); - window->DC.CursorPos = window->Pos - window->Scroll + local_pos; - window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->Pos = ImTrunc(ImVec2(settings->Pos.x, settings->Pos.y)); + if (settings->Size.x > 0 && settings->Size.y > 0) + window->Size = window->SizeFull = ImTrunc(ImVec2(settings->Size.x, settings->Size.y)); + window->Collapsed = settings->Collapsed; } -void ImGui::SetCursorPosX(float x) +static void UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags) { - ImGuiWindow *window = GetCurrentWindow(); - window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; - window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); -} + ImGuiContext& g = *GImGui; -void ImGui::SetCursorPosY(float y) -{ - ImGuiWindow *window = GetCurrentWindow(); - window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; - window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); + const bool new_is_explicit_child = (new_flags & ImGuiWindowFlags_ChildWindow) != 0 && ((new_flags & ImGuiWindowFlags_Popup) == 0 || (new_flags & ImGuiWindowFlags_ChildMenu) != 0); + const bool child_flag_changed = new_is_explicit_child != window->IsExplicitChild; + if ((just_created || child_flag_changed) && !new_is_explicit_child) + { + IM_ASSERT(!g.WindowsFocusOrder.contains(window)); + g.WindowsFocusOrder.push_back(window); + window->FocusOrder = (short)(g.WindowsFocusOrder.Size - 1); + } + else if (!just_created && child_flag_changed && new_is_explicit_child) + { + IM_ASSERT(g.WindowsFocusOrder[window->FocusOrder] == window); + for (int n = window->FocusOrder + 1; n < g.WindowsFocusOrder.Size; n++) + g.WindowsFocusOrder[n]->FocusOrder--; + g.WindowsFocusOrder.erase(g.WindowsFocusOrder.Data + window->FocusOrder); + window->FocusOrder = -1; + } + window->IsExplicitChild = new_is_explicit_child; } -ImVec2 ImGui::GetCursorStartPos() +static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.CursorStartPos - window->Pos; -} + // Initial window state with e.g. default/arbitrary window position + // Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->Pos = main_viewport->Pos + ImVec2(60, 60); + window->Size = window->SizeFull = ImVec2(0, 0); + window->SetWindowPosAllowFlags = window->SetWindowSizeAllowFlags = window->SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; -ImVec2 ImGui::GetCursorScreenPos() -{ - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.CursorPos; + if (settings != NULL) + { + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + ApplyWindowSettings(window, settings); + } + window->DC.CursorStartPos = window->DC.CursorMaxPos = window->DC.IdealMaxPos = window->Pos; // So first call to CalcWindowContentSizes() doesn't return crazy values + + if ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + { + window->AutoFitFramesX = window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } + else + { + if (window->Size.x <= 0.0f) + window->AutoFitFramesX = 2; + if (window->Size.y <= 0.0f) + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); + } } -void ImGui::SetCursorScreenPos(const ImVec2 &screen_pos) +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags) { - ImGuiWindow *window = GetCurrentWindow(); - window->DC.CursorPos = screen_pos; - window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + // Create window the first time + //IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + ImGuiWindowSettings* settings = NULL; + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + if ((settings = ImGui::FindWindowSettingsByWindow(window)) != 0) + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + + InitOrLoadWindowSettings(window, settings); + + if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) + g.Windows.push_front(window); // Quite slow but rare and only once + else + g.Windows.push_back(window); + + return window; } -float ImGui::GetScrollX() +static inline ImVec2 CalcWindowMinSize(ImGuiWindow* window) { - return GImGui->CurrentWindow->Scroll.x; + // We give windows non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + // FIXME: Essentially we want to restrict manual resizing to WindowMinSize+Decoration, and allow api resizing to be smaller. + // Perhaps should tend further a neater test for this. + ImGuiContext& g = *GImGui; + ImVec2 size_min; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) + { + size_min.x = (window->ChildFlags & ImGuiChildFlags_ResizeX) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = (window->ChildFlags & ImGuiChildFlags_ResizeY) ? g.Style.WindowMinSize.y : 4.0f; + } + else + { + size_min.x = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.y : 4.0f; + } + + // Reduce artifacts with very small windows + ImGuiWindow* window_for_height = window; + size_min.y = ImMax(size_min.y, window_for_height->TitleBarHeight + window_for_height->MenuBarHeight + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); + return size_min; } -float ImGui::GetScrollY() +static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& size_desired) { - return GImGui->CurrentWindow->Scroll.y; + ImGuiContext& g = *GImGui; + ImVec2 new_size = size_desired; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + // See comments in SetNextWindowSizeConstraints() for details about setting size_min an size_max. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + new_size.x = IM_TRUNC(new_size.x); + new_size.y = IM_TRUNC(new_size.y); + } + + // Minimum size + ImVec2 size_min = CalcWindowMinSize(window); + return ImMax(new_size, size_min); } -float ImGui::GetScrollMaxX() +static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_current, ImVec2* content_size_ideal) { - return GetScrollMaxX(GImGui->CurrentWindow); + bool preserve_old_content_sizes = false; + if (window->Collapsed && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + preserve_old_content_sizes = true; + else if (window->Hidden && window->HiddenFramesCannotSkipItems == 0 && window->HiddenFramesCanSkipItems > 0) + preserve_old_content_sizes = true; + if (preserve_old_content_sizes) + { + *content_size_current = window->ContentSize; + *content_size_ideal = window->ContentSizeIdeal; + return; + } + + content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); + content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); + content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); + content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); } -float ImGui::GetScrollMaxY() +static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents) { - return GetScrollMaxY(GImGui->CurrentWindow); + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + const float decoration_w_without_scrollbars = window->DecoOuterSizeX1 + window->DecoOuterSizeX2 - window->ScrollbarSizes.x; + const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y; + ImVec2 size_pad = window->WindowPadding * 2.0f; + ImVec2 size_desired = size_contents + size_pad + ImVec2(decoration_w_without_scrollbars, decoration_h_without_scrollbars); + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Tooltip always resize + return size_desired; + } + else + { + // Maximum window size is determined by the viewport size or monitor size + ImVec2 size_min = CalcWindowMinSize(window); + ImVec2 size_max = ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) ? ImVec2(FLT_MAX, FLT_MAX) : ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f; + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, size_max); + + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one axis may be auto-fit when calculating scrollbars, + // we may need to compute/store three variants of size_auto_fit, for x/y/xy. + // Here we implement a workaround for child windows only, but a full solution would apply to normal windows as well: + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && !(window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.y = window->SizeFull.y; + else if (!(window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.x = window->SizeFull.x; + + // When the window cannot fit all contents (either because of constraints, either because screen is too small), + // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. + ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); + bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - decoration_w_without_scrollbars < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); + bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_h_without_scrollbars < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (will_have_scrollbar_x) + size_auto_fit.y += style.ScrollbarSize; + if (will_have_scrollbar_y) + size_auto_fit.x += style.ScrollbarSize; + return size_auto_fit; + } } -void ImGui::SetScrollX(float scroll_x) +ImVec2 ImGui::CalcWindowNextAutoFitSize(ImGuiWindow* window) { - ImGuiWindow *window = GetCurrentWindow(); - window->ScrollTarget.x = scroll_x; - window->ScrollTargetCenterRatio.x = 0.0f; + ImVec2 size_contents_current; + ImVec2 size_contents_ideal; + CalcWindowContentSizes(window, &size_contents_current, &size_contents_ideal); + ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, size_contents_ideal); + ImVec2 size_final = CalcWindowSizeAfterConstraint(window, size_auto_fit); + return size_final; } -void ImGui::SetScrollY(float scroll_y) +static ImGuiCol GetWindowBgColorIdx(ImGuiWindow* window) { - ImGuiWindow *window = GetCurrentWindow(); - window->ScrollTarget.y = scroll_y + window->TitleBarHeight() + window->MenuBarHeight(); // title bar height canceled out when using ScrollTargetRelY - window->ScrollTargetCenterRatio.y = 0.0f; + if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + return ImGuiCol_PopupBg; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + return ImGuiCol_ChildBg; + return ImGuiCol_WindowBg; } -void ImGui::SetScrollFromPosY(float pos_y, float center_y_ratio) +static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) { - // We store a target position so centering can occur on the next frame when we are guaranteed to have a known window size - ImGuiWindow *window = GetCurrentWindow(); - IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); - window->ScrollTarget.y = (float) (int) (pos_y + window->Scroll.y); - window->ScrollTargetCenterRatio.y = center_y_ratio; - - // Minor hack to to make scrolling to top/bottom of window take account of WindowPadding, it looks more right to the user this way - if (center_y_ratio <= 0.0f && window->ScrollTarget.y <= window->WindowPadding.y) - window->ScrollTarget.y = 0.0f; - else if (center_y_ratio >= 1.0f && window->ScrollTarget.y >= window->SizeContents.y - window->WindowPadding.y + GImGui->Style.ItemSpacing.y) - window->ScrollTarget.y = window->SizeContents.y; + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = pos_max - pos_min; + ImVec2 size_constrained = CalcWindowSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; } -// center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. -void ImGui::SetScrollHere(float center_y_ratio) +// Data for resizing from resize grip / corner +struct ImGuiResizeGripDef { - ImGuiWindow *window = GetCurrentWindow(); - float target_y = window->DC.CursorPosPrevLine.y - window->Pos.y; // Top of last item, in window space - target_y += (window->DC.PrevLineHeight * center_y_ratio) + (GImGui->Style.ItemSpacing.y * (center_y_ratio - 0.5f) * 2.0f); // Precisely aim above, in the middle or below the last line. - SetScrollFromPosY(target_y, center_y_ratio); -} + ImVec2 CornerPosN; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; +}; +static const ImGuiResizeGripDef resize_grip_def[4] = +{ + { ImVec2(1, 1), ImVec2(-1, -1), 0, 3 }, // Lower-right + { ImVec2(0, 1), ImVec2(+1, -1), 3, 6 }, // Lower-left + { ImVec2(0, 0), ImVec2(+1, +1), 6, 9 }, // Upper-left (Unused) + { ImVec2(1, 0), ImVec2(-1, +1), 9, 12 } // Upper-right (Unused) +}; -void ImGui::ActivateItem(ImGuiID id) +// Data for resizing from borders +struct ImGuiResizeBorderDef { - ImGuiContext &g = *GImGui; - g.NavNextActivateId = id; -} + ImVec2 InnerDir; // Normal toward inside + ImVec2 SegmentN1, SegmentN2; // End positions, normalized (0,0: upper left) + float OuterAngle; // Angle toward outside +}; +static const ImGuiResizeBorderDef resize_border_def[4] = +{ + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down +}; -void ImGui::SetKeyboardFocusHere(int offset) +static ImRect GetResizeBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) { - IM_ASSERT(offset >= -1); // -1 is allowed but not below - ImGuiWindow *window = GetCurrentWindow(); - window->FocusIdxAllRequestNext = window->FocusIdxAllCounter + 1 + offset; - window->FocusIdxTabRequestNext = INT_MAX; + ImRect rect = window->Rect(); + if (thickness == 0.0f) + rect.Max -= ImVec2(1, 1); + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } + IM_ASSERT(0); + return ImRect(); } -void ImGui::SetItemDefaultFocus() +// 0..3: corners (Lower-right, Lower-left, Unused, Unused) +ImGuiID ImGui::GetWindowResizeCornerID(ImGuiWindow* window, int n) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (!window->Appearing) - return; - if (g.NavWindow == window->RootWindowForNav && (g.NavInitRequest || g.NavInitResultId != 0) && g.NavLayer == g.NavWindow->DC.NavLayerCurrent) - { - g.NavInitRequest = false; - g.NavInitResultId = g.NavWindow->DC.LastItemId; - g.NavInitResultRectRel = ImRect(g.NavWindow->DC.LastItemRect.Min - g.NavWindow->Pos, g.NavWindow->DC.LastItemRect.Max - g.NavWindow->Pos); - NavUpdateAnyRequestFlag(); - if (!IsItemVisible()) - SetScrollHere(); - } + IM_ASSERT(n >= 0 && n < 4); + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; } -void ImGui::SetStateStorage(ImGuiStorage *tree) +// Borders (Left, Right, Up, Down) +ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) { - ImGuiWindow *window = GetCurrentWindow(); - window->DC.StateStorage = tree ? tree : &window->StateStorage; + IM_ASSERT(dir >= 0 && dir < 4); + int n = (int)dir + 4; + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; } -ImGuiStorage *ImGui::GetStateStorage() +// Handle resize for: Resize Grips, Borders, Gamepad +// Return true when using auto-fit (double-click on resize grip) +static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.StateStorage; -} + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; -void ImGui::TextV(const char *fmt, va_list args) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + return false; + if (window->WasActive == false) // Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window. + return false; + + int ret_auto_fit_mask = 0x00; + const float grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = (resize_grip_count > 0) ? IM_TRUNC(grip_draw_size * 0.75f) : 0.0f; + const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_HOVER_PADDING : 0.0f; + + ImRect clamp_rect = visibility_rect; + const bool window_move_from_title_bar = g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar); + if (window_move_from_title_bar) + clamp_rect.Min.y -= window->TitleBarHeight; + + ImVec2 pos_target(FLT_MAX, FLT_MAX); + ImVec2 size_target(FLT_MAX, FLT_MAX); + + // Resize grips and borders are on layer 1 + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Manual resize grips + PushID("#RESIZE"); + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& def = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, def.CornerPosN); + + // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window + bool hovered, held; + ImRect resize_rect(corner - def.InnerDir * grip_hover_outer_size, corner + def.InnerDir * grip_hover_inner_size); + if (resize_rect.Min.x > resize_rect.Max.x) ImSwap(resize_rect.Min.x, resize_rect.Max.x); + if (resize_rect.Min.y > resize_rect.Max.y) ImSwap(resize_rect.Min.y, resize_rect.Max.y); + ImGuiID resize_grip_id = window->GetID(resize_grip_n); // == GetWindowResizeCornerID() + ItemAdd(resize_rect, resize_grip_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(resize_rect, resize_grip_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered || held) + g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; + + if (held && g.IO.MouseDoubleClicked[0]) + { + // Auto-fit when double-clicking + size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); + ret_auto_fit_mask = 0x03; // Both axises + ClearActiveID(); + } + else if (held) + { + // Resize from any of the four corners + // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position + ImVec2 clamp_min = ImVec2(def.CornerPosN.x == 1.0f ? clamp_rect.Min.x : -FLT_MAX, (def.CornerPosN.y == 1.0f || (def.CornerPosN.y == 0.0f && window_move_from_title_bar)) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? clamp_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? clamp_rect.Max.y : +FLT_MAX); + ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip + corner_target = ImClamp(corner_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); + } + + // Only lower-left grip is visible before hovering/activating + if (resize_grip_n == 0 || held || hovered) + resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); + } + + int resize_border_mask = 0x00; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + resize_border_mask |= ((window->ChildFlags & ImGuiChildFlags_ResizeX) ? 0x02 : 0) | ((window->ChildFlags & ImGuiChildFlags_ResizeY) ? 0x08 : 0); + else + resize_border_mask = g.IO.ConfigWindowsResizeFromEdges ? 0x0F : 0x00; + for (int border_n = 0; border_n < 4; border_n++) + { + if ((resize_border_mask & (1 << border_n)) == 0) + continue; + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + + bool hovered, held; + ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, WINDOWS_HOVER_PADDING); + ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() + ItemAdd(border_rect, border_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered && g.HoveredIdTimer <= WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) + hovered = false; + if (hovered || held) + g.MouseCursor = (axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; + if (held && g.IO.MouseDoubleClicked[0]) + { + // Double-clicking bottom or right border auto-fit on this axis + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one side may be auto-fit when calculating scrollbars. + // FIXME: Support top and right borders: rework CalcResizePosSizeFromAnyCorner() to be reusable in both cases. + if (border_n == 1 || border_n == 3) // Right and bottom border + { + size_target[axis] = CalcWindowSizeAfterConstraint(window, size_auto_fit)[axis]; + ret_auto_fit_mask |= (1 << axis); + hovered = held = false; // So border doesn't show highlighted at new position + } + ClearActiveID(); + } + else if (held) + { + // Switch to relative resizing mode when border geometry moved (e.g. resizing a child altering parent scroll), in order to avoid resizing feedback loop. + // Currently only using relative mode on resizable child windows, as the problem to solve is more likely noticeable for them, but could apply for all windows eventually. + // FIXME: May want to generalize this idiom at lower-level, so more widgets can use it! + const bool just_scrolled_manually_while_resizing = (g.WheelingWindow != NULL && g.WheelingWindowScrolledFrame == g.FrameCount && IsWindowChildOf(window, g.WheelingWindow, false)); + if (g.ActiveIdIsJustActivated || just_scrolled_manually_while_resizing) + { + g.WindowResizeBorderExpectedRect = border_rect; + g.WindowResizeRelativeMode = false; + } + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && memcmp(&g.WindowResizeBorderExpectedRect, &border_rect, sizeof(ImRect)) != 0) + g.WindowResizeRelativeMode = true; + + const ImVec2 border_curr = (window->Pos + ImMin(def.SegmentN1, def.SegmentN2) * window->Size); + const float border_target_rel_mode_for_axis = border_curr[axis] + g.IO.MouseDelta[axis]; + const float border_target_abs_mode_for_axis = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + WINDOWS_HOVER_PADDING; // Match ButtonBehavior() padding above. + + // Use absolute mode position + ImVec2 border_target = window->Pos; + border_target[axis] = border_target_abs_mode_for_axis; + + // Use relative mode target for child window, ignore resize when moving back toward the ideal absolute position. + bool ignore_resize = false; + if (g.WindowResizeRelativeMode) + { + //GetForegroundDrawList()->AddText(GetMainViewport()->WorkPos, IM_COL32_WHITE, "Relative Mode"); + border_target[axis] = border_target_rel_mode_for_axis; + if (g.IO.MouseDelta[axis] == 0.0f || (g.IO.MouseDelta[axis] > 0.0f) == (border_target_rel_mode_for_axis > border_target_abs_mode_for_axis)) + ignore_resize = true; + } + + // Clamp, apply + ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); + border_target = ImClamp(border_target, clamp_min, clamp_max); + if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent + { + ImGuiWindow* parent_window = window->ParentWindow; + ImGuiWindowFlags parent_flags = parent_window->Flags; + ImRect border_limit_rect = parent_window->InnerRect; + border_limit_rect.Expand(ImVec2(-ImMax(parent_window->WindowPadding.x, parent_window->WindowBorderSize), -ImMax(parent_window->WindowPadding.y, parent_window->WindowBorderSize))); + if ((axis == ImGuiAxis_X) && ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar))) + border_target.x = ImClamp(border_target.x, border_limit_rect.Min.x, border_limit_rect.Max.x); + if ((axis == ImGuiAxis_Y) && (parent_flags & ImGuiWindowFlags_NoScrollbar)) + border_target.y = ImClamp(border_target.y, border_limit_rect.Min.y, border_limit_rect.Max.y); + } + if (!ignore_resize) + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + } + if (hovered) + *border_hovered = border_n; + if (held) + *border_held = border_n; + } + PopID(); + + // Restore nav layer + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + + // Navigation resize (keyboard/gamepad) + // FIXME: This cannot be moved to NavUpdateWindowing() because CalcWindowSizeAfterConstraint() need to callback into user. + // Not even sure the callback works here. + if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindow == window) + { + ImVec2 nav_resize_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && g.IO.KeyShift) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown); + if (nav_resize_dir.x != 0.0f || nav_resize_dir.y != 0.0f) + { + const float NAV_RESIZE_SPEED = 600.0f; + const float resize_step = NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaSize += nav_resize_dir * resize_step; + g.NavWindowingAccumDeltaSize = ImMax(g.NavWindowingAccumDeltaSize, clamp_rect.Min - window->Pos - window->Size); // We need Pos+Size >= clmap_rect.Min, so Size >= clmap_rect.Min - Pos, so size_delta >= clmap_rect.Min - window->Pos - window->Size + g.NavWindowingToggleLayer = false; + g.NavDisableMouseHover = true; + resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaSize); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. + size_target = CalcWindowSizeAfterConstraint(window, window->SizeFull + accum_floored); + g.NavWindowingAccumDeltaSize -= accum_floored; + } + } + } + + // Apply back modified position/size to window + const ImVec2 curr_pos = window->Pos; + const ImVec2 curr_size = window->SizeFull; + if (size_target.x != FLT_MAX && (window->Size.x != size_target.x || window->SizeFull.x != size_target.x)) + window->Size.x = window->SizeFull.x = size_target.x; + if (size_target.y != FLT_MAX && (window->Size.y != size_target.y || window->SizeFull.y != size_target.y)) + window->Size.y = window->SizeFull.y = size_target.y; + if (pos_target.x != FLT_MAX && window->Pos.x != ImTrunc(pos_target.x)) + window->Pos.x = ImTrunc(pos_target.x); + if (pos_target.y != FLT_MAX && window->Pos.y != ImTrunc(pos_target.y)) + window->Pos.y = ImTrunc(pos_target.y); + if (curr_pos.x != window->Pos.x || curr_pos.y != window->Pos.y || curr_size.x != window->SizeFull.x || curr_size.y != window->SizeFull.y) + MarkIniSettingsDirty(window); - ImGuiContext &g = *GImGui; - const char *text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - TextUnformatted(g.TempBuffer, text_end); + // Recalculate next expected border expected coordinates + if (*border_held != -1) + g.WindowResizeBorderExpectedRect = GetResizeBorderRect(window, *border_held, grip_hover_inner_size, WINDOWS_HOVER_PADDING); + + return ret_auto_fit_mask; } -void ImGui::Text(const char *fmt, ...) +static inline void ClampWindowPos(ImGuiWindow* window, const ImRect& visibility_rect) { - va_list args; - va_start(args, fmt); - TextV(fmt, args); - va_end(args); + ImGuiContext& g = *GImGui; + ImVec2 size_for_clamping = window->Size; + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + size_for_clamping.y = window->TitleBarHeight; + window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); } -void ImGui::TextColoredV(const ImVec4 &col, const char *fmt, va_list args) +static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU32 border_col, float border_size) { - PushStyleColor(ImGuiCol_Text, col); - TextV(fmt, args); - PopStyleColor(); + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const float rounding = window->WindowRounding; + const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(border_col, ImDrawFlags_None, border_size); } -void ImGui::TextColored(const ImVec4 &col, const char *fmt, ...) +static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) { - va_list args; - va_start(args, fmt); - TextColoredV(col, fmt, args); - va_end(args); + ImGuiContext& g = *GImGui; + const float border_size = window->WindowBorderSize; + const ImU32 border_col = GetColorU32(ImGuiCol_Border); + if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0) + window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, 0, window->WindowBorderSize); + else if (border_size > 0.0f) + { + if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border. + RenderWindowOuterSingleBorder(window, 1, border_col, border_size); + if (window->ChildFlags & ImGuiChildFlags_ResizeY) + RenderWindowOuterSingleBorder(window, 3, border_col, border_size); + } + if (window->ResizeBorderHovered != -1 || window->ResizeBorderHeld != -1) + { + const int border_n = (window->ResizeBorderHeld != -1) ? window->ResizeBorderHeld : window->ResizeBorderHovered; + const ImU32 border_col_resizing = GetColorU32((window->ResizeBorderHeld != -1) ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0f, window->WindowBorderSize)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + { + float y = window->Pos.y + window->TitleBarHeight - 1; + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), border_col, g.Style.FrameBorderSize); + } } -void ImGui::TextDisabledV(const char *fmt, va_list args) +// Draw background and borders +// Draw and handle scrollbars +void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size) { - PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); - TextV(fmt, args); - PopStyleColor(); + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + // Ensure that ScrollBar doesn't read last frame's SkipItems + IM_ASSERT(window->BeginCount == 0); + window->SkipItems = false; + + // Draw window + handle manual resize + // As we highlight the title bar when want_focus is set, multiple reappearing windows will have their title bar highlighted on their reappearing frame. + const float window_rounding = window->WindowRounding; + const float window_border_size = window->WindowBorderSize; + if (window->Collapsed) + { + // Title bar only + const float backup_border_size = style.FrameBorderSize; + g.Style.FrameBorderSize = window->WindowBorderSize; + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); + g.Style.FrameBorderSize = backup_border_size; + } + else + { + // Window background + if (!(flags & ImGuiWindowFlags_NoBackground)) + { + ImU32 bg_col = GetColorU32(GetWindowBgColorIdx(window)); + bool override_alpha = false; + float alpha = 1.0f; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasBgAlpha) + { + alpha = g.NextWindowData.BgAlphaVal; + override_alpha = true; + } + if (override_alpha) + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); + window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + { + ImU32 title_bar_col = GetColorU32(title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags_RoundCornersTop); + } + + // Menu bar + if (flags & ImGuiWindowFlags_MenuBar) + { + ImRect menu_bar_rect = window->MenuBarRect(); + menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. + window->DrawList->AddRectFilled(menu_bar_rect.Min + ImVec2(window_border_size, 0), menu_bar_rect.Max - ImVec2(window_border_size, 0), GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop); + if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) + window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Scrollbars + if (window->ScrollbarX) + Scrollbar(ImGuiAxis_X); + if (window->ScrollbarY) + Scrollbar(ImGuiAxis_Y); + + // Render resize grips (after their input handling so we don't have a frame of latency) + if (handle_borders_and_resize_grips && !(flags & ImGuiWindowFlags_NoResize)) + { + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImU32 col = resize_grip_col[resize_grip_n]; + if ((col & IM_COL32_A_MASK) == 0) + continue; + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPosN); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, resize_grip_draw_size) : ImVec2(resize_grip_draw_size, window_border_size))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(resize_grip_draw_size, window_border_size) : ImVec2(window_border_size, resize_grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(col); + } + } + + // Borders + if (handle_borders_and_resize_grips) + RenderWindowOuterBorders(window); + } } -void ImGui::TextDisabled(const char *fmt, ...) +// Render title text, collapse button, close button +void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) { - va_list args; - va_start(args, fmt); - TextDisabledV(fmt, args); - va_end(args); -} - -void ImGui::TextWrappedV(const char *fmt, va_list args) -{ - bool need_wrap = (GImGui->CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position is one ia already set - if (need_wrap) - PushTextWrapPos(0.0f); - TextV(fmt, args); - if (need_wrap) - PopTextWrapPos(); -} - -void ImGui::TextWrapped(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - TextWrappedV(fmt, args); - va_end(args); -} - -void ImGui::TextUnformatted(const char *text, const char *text_end) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext &g = *GImGui; - IM_ASSERT(text != NULL); - const char *text_begin = text; - if (text_end == NULL) - text_end = text + strlen(text); // FIXME-OPT - - const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrentLineTextBaseOffset); - const float wrap_pos_x = window->DC.TextWrapPos; - const bool wrap_enabled = wrap_pos_x >= 0.0f; - if (text_end - text > 2000 && !wrap_enabled) - { - // Long text! - // Perform manual coarse clipping to optimize for long multi-line text - // From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. - // We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line. - const char *line = text; - const float line_height = GetTextLineHeight(); - const ImRect clip_rect = window->ClipRect; - ImVec2 text_size(0, 0); + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; - if (text_pos.y <= clip_rect.Max.y) - { - ImVec2 pos = text_pos; - - // Lines to skip (can't skip when logging text) - if (!g.LogEnabled) - { - int lines_skippable = (int) ((clip_rect.Min.y - text_pos.y) / line_height); - if (lines_skippable > 0) - { - int lines_skipped = 0; - while (line < text_end && lines_skipped < lines_skippable) - { - const char *line_end = strchr(line, '\n'); - if (!line_end) - line_end = text_end; - line = line_end + 1; - lines_skipped++; - } - pos.y += lines_skipped * line_height; - } - } - - // Lines to render - if (line < text_end) - { - ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); - while (line < text_end) - { - const char *line_end = strchr(line, '\n'); - if (IsClippedEx(line_rect, 0, false)) - break; - - const ImVec2 line_size = CalcTextSize(line, line_end, false); - text_size.x = ImMax(text_size.x, line_size.x); - RenderText(pos, line, line_end, false); - if (!line_end) - line_end = text_end; - line = line_end + 1; - line_rect.Min.y += line_height; - line_rect.Max.y += line_height; - pos.y += line_height; - } - - // Count remaining lines - int lines_skipped = 0; - while (line < text_end) - { - const char *line_end = strchr(line, '\n'); - if (!line_end) - line_end = text_end; - line = line_end + 1; - lines_skipped++; - } - pos.y += lines_skipped * line_height; - } - - text_size.y += (pos - text_pos).y; - } - - ImRect bb(text_pos, text_pos + text_size); - ItemSize(bb); - ItemAdd(bb, 0); - } - else - { - const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; - const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); - - // Account of baseline offset - ImRect bb(text_pos, text_pos + text_size); - ItemSize(text_size); - if (!ItemAdd(bb, 0)) - return; - - // Render (we don't hide text after ## in this end-user function) - RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); - } -} - -void ImGui::AlignTextToFramePadding() -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext &g = *GImGui; - window->DC.CurrentLineHeight = ImMax(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y * 2); - window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.CurrentLineTextBaseOffset, g.Style.FramePadding.y); -} - -// Add a label+text combo aligned to other label+value widgets -void ImGui::LabelTextV(const char *label, const char *fmt, va_list args) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2)); - const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y * 2) + label_size); - ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, 0)) - return; - - // Render - const char *value_text_begin = &g.TempBuffer[0]; - const char *value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f, 0.5f)); - if (label_size.x > 0.0f) - RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); -} - -void ImGui::LabelText(const char *label, const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - LabelTextV(label, fmt, args); - va_end(args); -} - -bool ImGui::ButtonBehavior(const ImRect &bb, ImGuiID id, bool *out_hovered, bool *out_held, ImGuiButtonFlags flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - - if (flags & ImGuiButtonFlags_Disabled) - { - if (out_hovered) - *out_hovered = false; - if (out_held) - *out_held = false; - if (g.ActiveId == id) - ClearActiveID(); - return false; - } - - // Default behavior requires click+release on same spot - if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0) - flags |= ImGuiButtonFlags_PressedOnClickRelease; - - ImGuiWindow *backup_hovered_window = g.HoveredWindow; - if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) - g.HoveredWindow = window; - - bool pressed = false; - bool hovered = ItemHoverable(bb, id); - - // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button - if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && g.DragDropActive && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) - if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - { - hovered = true; - SetHoveredID(id); - if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy - { - pressed = true; - FocusWindow(window); - } - } - - if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) - g.HoveredWindow = backup_hovered_window; - - // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. - if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) - hovered = false; - - // Mouse - if (hovered) - { - if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) - { - // | CLICKING | HOLDING with ImGuiButtonFlags_Repeat - // PressedOnClickRelease | * | .. (NOT on release) <-- MOST COMMON! (*) only if both click/release were over bounds - // PressedOnClick | | .. - // PressedOnRelease | | .. (NOT on release) - // PressedOnDoubleClick | | .. - // FIXME-NAV: We don't honor those different behaviors. - if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0]) - { - SetActiveID(id, window); - if (!(flags & ImGuiButtonFlags_NoNavFocus)) - SetFocusID(id, window); - FocusWindow(window); - } - if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0])) - { - pressed = true; - if (flags & ImGuiButtonFlags_NoHoldingActiveID) - ClearActiveID(); - else - SetActiveID(id, window); // Hold on ID - FocusWindow(window); - } - if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0]) - { - if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps - pressed = true; - ClearActiveID(); - } - - // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). - // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. - if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true)) - pressed = true; - } - - if (pressed) - g.NavDisableHighlight = true; - } - - // Gamepad/Keyboard navigation - // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. - if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) - hovered = true; - - if (g.NavActivateDownId == id) - { - bool nav_activated_by_code = (g.NavActivateId == id); - bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); - if (nav_activated_by_code || nav_activated_by_inputs) - pressed = true; - if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id) - { - // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. - g.NavActivateId = id; // This is so SetActiveId assign a Nav source - SetActiveID(id, window); - if (!(flags & ImGuiButtonFlags_NoNavFocus)) - SetFocusID(id, window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - } - } - - bool held = false; - if (g.ActiveId == id) - { - if (g.ActiveIdSource == ImGuiInputSource_Mouse) - { - if (g.ActiveIdIsJustActivated) - g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; - if (g.IO.MouseDown[0]) - { - held = true; - } - else - { - if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease)) - if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps - if (!g.DragDropActive) - pressed = true; - ClearActiveID(); - } - if (!(flags & ImGuiButtonFlags_NoNavFocus)) - g.NavDisableHighlight = true; - } - else if (g.ActiveIdSource == ImGuiInputSource_Nav) - { - if (g.NavActivateDownId != id) - ClearActiveID(); - } - } - - if (out_hovered) - *out_hovered = hovered; - if (out_held) - *out_held = held; - - return pressed; -} - -bool ImGui::ButtonEx(const char *label, const ImVec2 &size_arg, ImGuiButtonFlags flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - - ImVec2 pos = window->DC.CursorPos; - if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrentLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) - pos.y += window->DC.CurrentLineTextBaseOffset - style.FramePadding.y; - ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); - - const ImRect bb(pos, pos + size); - ItemSize(bb, style.FramePadding.y); - if (!ItemAdd(bb, id)) - return false; - - if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) - flags |= ImGuiButtonFlags_Repeat; - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); - - // Render - const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : - ImGuiCol_Button); - RenderNavHighlight(bb, id); - RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); - RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); - - // Automatically close popups - // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) - // CloseCurrentPopup(); - - return pressed; -} - -bool ImGui::Button(const char *label, const ImVec2 &size_arg) -{ - return ButtonEx(label, size_arg, 0); -} - -// Small buttons fits within text without additional vertical spacing. -bool ImGui::SmallButton(const char *label) -{ - ImGuiContext &g = *GImGui; - float backup_padding_y = g.Style.FramePadding.y; - g.Style.FramePadding.y = 0.0f; - bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine); - g.Style.FramePadding.y = backup_padding_y; - return pressed; -} - -// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. -// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) -bool ImGui::InvisibleButton(const char *str_id, const ImVec2 &size_arg) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - const ImGuiID id = window->GetID(str_id); - ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - ItemSize(bb); - if (!ItemAdd(bb, id)) - return false; - - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); - - return pressed; -} - -// Button to close a window -bool ImGui::CloseButton(ImGuiID id, const ImVec2 &pos, float radius) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; + const bool has_close_button = (p_open != NULL); + const bool has_collapse_button = !(flags & ImGuiWindowFlags_NoCollapse) && (style.WindowMenuButtonPosition != ImGuiDir_None); - // We intentionally allow interaction when clipped so that a mechanical Alt,Right,Validate sequence close a window. - // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). - const ImRect bb(pos - ImVec2(radius, radius), pos + ImVec2(radius, radius)); - bool is_clipped = !ItemAdd(bb, id); + // Close & Collapse button are on the Menu NavLayer and don't default focus (unless there's nothing else on that layer) + // FIXME-NAV: Might want (or not?) to set the equivalent of ImGuiButtonFlags_NoNavFocus so that mouse clicks on standard title bar items don't necessarily set nav/keyboard ref? + const ImGuiItemFlags item_flags_backup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); - if (is_clipped) - return pressed; + // Layout buttons + // FIXME: Would be nice to generalize the subtleties expressed here into reusable code. + float pad_l = style.FramePadding.x; + float pad_r = style.FramePadding.x; + float button_sz = g.FontSize; + ImVec2 close_button_pos; + ImVec2 collapse_button_pos; + if (has_close_button) + { + close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Right) + { + collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Left) + { + collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y + style.FramePadding.y); + pad_l += button_sz + style.ItemInnerSpacing.x; + } - // Render - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_CloseButtonActive : hovered ? ImGuiCol_CloseButtonHovered : - ImGuiCol_CloseButton); - ImVec2 center = bb.GetCenter(); - window->DrawList->AddCircleFilled(center, ImMax(2.0f, radius), col, 12); + // Collapse button (submitting first so it gets priority when choosing a navigation init fallback) + if (has_collapse_button) + if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos)) + window->WantCollapseToggle = true; // Defer actual collapsing to next frame as we are too far in the Begin() function + + // Close button + if (has_close_button) + if (CloseButton(window->GetID("#CLOSE"), close_button_pos)) + *p_open = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + g.CurrentItemFlags = item_flags_backup; + + // Title bar text (with: horizontal alignment, avoiding collapse/close button, optional "unsaved document" marker) + // FIXME: Refactor text alignment facilities along with RenderText helpers, this is WAY too much messy code.. + const float marker_size_x = (flags & ImGuiWindowFlags_UnsavedDocument) ? button_sz * 0.80f : 0.0f; + const ImVec2 text_size = CalcTextSize(name, NULL, true) + ImVec2(marker_size_x, 0.0f); + + // As a nice touch we try to ensure that centered title text doesn't get affected by visibility of Close/Collapse button, + // while uncentered title text will still reach edges correctly. + if (pad_l > style.FramePadding.x) + pad_l += g.Style.ItemInnerSpacing.x; + if (pad_r > style.FramePadding.x) + pad_r += g.Style.ItemInnerSpacing.x; + if (style.WindowTitleAlign.x > 0.0f && style.WindowTitleAlign.x < 1.0f) + { + float centerness = ImSaturate(1.0f - ImFabs(style.WindowTitleAlign.x - 0.5f) * 2.0f); // 0.0f on either edges, 1.0f on center + float pad_extend = ImMin(ImMax(pad_l, pad_r), title_bar_rect.GetWidth() - pad_l - pad_r - text_size.x); + pad_l = ImMax(pad_l, pad_extend * centerness); + pad_r = ImMax(pad_r, pad_extend * centerness); + } - const float cross_extent = (radius * 0.7071f) - 1.0f; - if (hovered) - { - center -= ImVec2(0.5f, 0.5f); - window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent), center + ImVec2(-cross_extent, -cross_extent), GetColorU32(ImGuiCol_Text)); - window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent), center + ImVec2(-cross_extent, +cross_extent), GetColorU32(ImGuiCol_Text)); - } - return pressed; + ImRect layout_r(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y, title_bar_rect.Max.x - pad_r, title_bar_rect.Max.y); + ImRect clip_r(layout_r.Min.x, layout_r.Min.y, ImMin(layout_r.Max.x + g.Style.ItemInnerSpacing.x, title_bar_rect.Max.x), layout_r.Max.y); + if (flags & ImGuiWindowFlags_UnsavedDocument) + { + ImVec2 marker_pos; + marker_pos.x = ImClamp(layout_r.Min.x + (layout_r.GetWidth() - text_size.x) * style.WindowTitleAlign.x + text_size.x, layout_r.Min.x, layout_r.Max.x); + marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5f; + if (marker_pos.x > layout_r.Min.x) + { + RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_Text)); + clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - (int)(marker_size_x * 0.5f)); + } + } + //if (g.IO.KeyShift) window->DrawList->AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + //if (g.IO.KeyCtrl) window->DrawList->AddRect(clip_r.Min, clip_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + RenderTextClipped(layout_r.Min, layout_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_r); +} + +void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window) +{ + window->ParentWindow = parent_window; + window->RootWindow = window->RootWindowPopupTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; + if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) + window->RootWindow = parent_window->RootWindow; + if (parent_window && (flags & ImGuiWindowFlags_Popup)) + window->RootWindowPopupTree = parent_window->RootWindowPopupTree; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight; + while (window->RootWindowForNav->ChildFlags & ImGuiChildFlags_NavFlattened) + { + IM_ASSERT(window->RootWindowForNav->ParentWindow != NULL); + window->RootWindowForNav = window->RootWindowForNav->ParentWindow; + } } -// [Internal] -bool ImGui::ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags) +// [EXPERIMENTAL] Called by Begin(). NextWindowData is valid at this point. +// This is designed as a toy/test-bed for +void ImGui::UpdateWindowSkipRefresh(ImGuiWindow* window) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (window->SkipItems) - return false; + ImGuiContext& g = *GImGui; + window->SkipRefresh = false; + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasRefreshPolicy) == 0) + return; + if (g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_TryToAvoidRefresh) + { + // FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. + if (window->Appearing) // If currently appearing + return; + if (window->Hidden) // If was hidden (previous frame) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnHover) && g.HoveredWindow) + if (window->RootWindow == g.HoveredWindow->RootWindow || IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, window)) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnFocus) && g.NavWindow) + if (window->RootWindow == g.NavWindow->RootWindow || IsWindowWithinBeginStackOf(g.NavWindow->RootWindow, window)) + return; + window->DrawList = NULL; + window->SkipRefresh = true; + } +} - const ImGuiStyle &style = g.Style; +static void SetWindowActiveForSkipRefresh(ImGuiWindow* window) +{ + window->Active = true; + for (ImGuiWindow* child : window->DC.ChildWindows) + if (!child->Hidden) + { + child->Active = child->SkipRefresh = true; + SetWindowActiveForSkipRefresh(child); + } +} + +// When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) +// should be positioned behind that modal window, unless the window was created inside the modal begin-stack. +// In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. +// - WindowA // FindBlockingModal() returns Modal1 +// - WindowB // .. returns Modal1 +// - Modal1 // .. returns Modal2 +// - WindowC // .. returns Modal2 +// - WindowD // .. returns Modal2 +// - Modal2 // .. returns Modal2 +// - WindowE // .. returns NULL +// Notes: +// - FindBlockingModal(NULL) == NULL is generally equivalent to GetTopMostPopupModal() == NULL. +// Only difference is here we check for ->Active/WasActive but it may be unnecessary. +ImGuiWindow* ImGui::FindBlockingModal(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= 0) + return NULL; + + // Find a modal that has common parent with specified window. Specified window should be positioned behind that modal. + for (ImGuiPopupData& popup_data : g.OpenPopupStack) + { + ImGuiWindow* popup_window = popup_data.Window; + if (popup_window == NULL || !(popup_window->Flags & ImGuiWindowFlags_Modal)) + continue; + if (!popup_window->Active && !popup_window->WasActive) // Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows. + continue; + if (window == NULL) // FindBlockingModal(NULL) test for if FocusWindow(NULL) is naturally possible via a mouse click. + return popup_window; + if (IsWindowWithinBeginStackOf(window, popup_window)) // Window may be over modal + continue; + return popup_window; // Place window right below first block modal + } + return NULL; +} - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + padding.x * 2.0f, g.FontSize + padding.y * 2.0f)); - ItemSize(bb, style.FramePadding.y); - if (!ItemAdd(bb, id)) - return false; +// Push a new Dear ImGui window to add widgets to. +// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. +// - Begin/End can be called multiple times during the frame with the same window name to append content. +// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). +// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. +// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. +// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. +bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + IM_ASSERT(name != NULL && name[0] != '\0'); // Window name required + IM_ASSERT(g.WithinFrameScope); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet + + // Find or create + ImGuiWindow* window = FindWindowByName(name); + const bool window_just_created = (window == NULL); + if (window_just_created) + window = CreateNewWindow(name, flags); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInWindow == window->ID) + IM_DEBUG_BREAK(); + + // Automatically disable manual moving/resizing when NoInputs is set + if ((flags & ImGuiWindowFlags_NoInputs) == ImGuiWindowFlags_NoInputs) + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + + const int current_frame = g.FrameCount; + const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + window->IsFallbackWindow = (g.CurrentWindowStack.Size == 0 && g.WithinFrameScopeWithImplicitWindow); + + // Update the Appearing flag + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed + window_just_activated_by_user |= (window != popup_ref.Window); + } + window->Appearing = window_just_activated_by_user; + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + // Update Flags, LastFrameActive, BeginOrderXXX fields + if (first_begin_of_the_frame) + { + UpdateWindowInFocusOrderList(window, window_just_created, flags); + window->Flags = (ImGuiWindowFlags)flags; + window->ChildFlags = (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : 0; + window->LastFrameActive = current_frame; + window->LastTimeActive = (float)g.Time; + window->BeginOrderWithinParent = 0; + window->BeginOrderWithinContext = (short)(g.WindowsActiveCount++); + } + else + { + flags = window->Flags; + } - const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : - ImGuiCol_Button); - RenderNavHighlight(bb, id); - RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); - RenderTriangle(bb.Min + padding, dir, 1.0f); + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack + ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; + ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; + IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); + + // We allow window memory to be compacted so recreate the base stack when needed. + if (window->IDStack.Size == 0) + window->IDStack.push_back(window->ID); + + // Add to stack + g.CurrentWindow = window; + ImGuiWindowStackData window_stack_data; + window_stack_data.Window = window; + window_stack_data.ParentLastItemDataBackup = g.LastItemData; + window_stack_data.StackSizesOnBegin.SetToContextState(&g); + window_stack_data.DisabledOverrideReenable = (flags & ImGuiWindowFlags_Tooltip) && (g.CurrentItemFlags & ImGuiItemFlags_Disabled); + g.CurrentWindowStack.push_back(window_stack_data); + if (flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth++; + + // Update ->RootWindow and others pointers (before any possible call to FocusWindow) + if (first_begin_of_the_frame) + { + UpdateWindowParentAndRootLinks(window, flags, parent_window); + window->ParentWindowInBeginStack = parent_window_in_stack; - return pressed; -} + // There's little point to expose a flag to set this: because the interesting cases won't be using parent_window_in_stack, + // e.g. linking a tool window in a standalone viewport to a document window, regardless of their Begin() stack parenting. (#6798) + window->ParentWindowForFocusRoute = (flags & ImGuiWindowFlags_ChildWindow) ? parent_window_in_stack : NULL; + } -void ImGui::Image(ImTextureID user_texture_id, const ImVec2 &size, const ImVec2 &uv0, const ImVec2 &uv1, const ImVec4 &tint_col, const ImVec4 &border_col) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - if (border_col.w > 0.0f) - bb.Max += ImVec2(2, 2); - ItemSize(bb); - if (!ItemAdd(bb, 0)) - return; - - if (border_col.w > 0.0f) - { - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); - window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col)); - } - else - { - window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); - } -} - -// frame_padding < 0: uses FramePadding from style (default) -// frame_padding = 0: no framing -// frame_padding > 0: set framing size -// The color used are the button colors. -bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2 &size, const ImVec2 &uv0, const ImVec2 &uv1, int frame_padding, const ImVec4 &bg_col, const ImVec4 &tint_col) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - - // Default to using texture ID as ID. User can still push string/integer prefixes. - // We could hash the size/uv to create a unique ID but that would prevent the user from animating UV. - PushID((void *) user_texture_id); - const ImGuiID id = window->GetID("#image"); - PopID(); - - const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float) frame_padding, (float) frame_padding) : style.FramePadding; - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2); - const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size); - ItemSize(bb); - if (!ItemAdd(bb, id)) - return false; - - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); - - // Render - const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : - ImGuiCol_Button); - RenderNavHighlight(bb, id); - RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float) ImMin(padding.x, padding.y), 0.0f, style.FrameRounding)); - if (bg_col.w > 0.0f) - window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col)); - window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col)); - - return pressed; -} - -// Start logging ImGui output to TTY -void ImGui::LogToTTY(int max_depth) -{ - ImGuiContext &g = *GImGui; - if (g.LogEnabled) - return; - ImGuiWindow *window = g.CurrentWindow; - - IM_ASSERT(g.LogFile == NULL); - g.LogFile = stdout; - g.LogEnabled = true; - g.LogStartDepth = window->DC.TreeDepth; - if (max_depth >= 0) - g.LogAutoExpandMaxDepth = max_depth; -} - -// Start logging ImGui output to given file -void ImGui::LogToFile(int max_depth, const char *filename) -{ - ImGuiContext &g = *GImGui; - if (g.LogEnabled) - return; - ImGuiWindow *window = g.CurrentWindow; - - if (!filename) - { - filename = g.IO.LogFilename; - if (!filename) - return; - } - - IM_ASSERT(g.LogFile == NULL); - g.LogFile = ImFileOpen(filename, "ab"); - if (!g.LogFile) - { - IM_ASSERT(g.LogFile != NULL); // Consider this an error - return; - } - g.LogEnabled = true; - g.LogStartDepth = window->DC.TreeDepth; - if (max_depth >= 0) - g.LogAutoExpandMaxDepth = max_depth; -} - -// Start logging ImGui output to clipboard -void ImGui::LogToClipboard(int max_depth) -{ - ImGuiContext &g = *GImGui; - if (g.LogEnabled) - return; - ImGuiWindow *window = g.CurrentWindow; - - IM_ASSERT(g.LogFile == NULL); - g.LogFile = NULL; - g.LogEnabled = true; - g.LogStartDepth = window->DC.TreeDepth; - if (max_depth >= 0) - g.LogAutoExpandMaxDepth = max_depth; -} + // Add to focus scope stack + PushFocusScope((window->ChildFlags & ImGuiChildFlags_NavFlattened) ? g.CurrentFocusScopeId : window->ID); + window->NavRootFocusScopeId = g.CurrentFocusScopeId; -void ImGui::LogFinish() -{ - ImGuiContext &g = *GImGui; - if (!g.LogEnabled) - return; - - LogText(IM_NEWLINE); - if (g.LogFile != NULL) - { - if (g.LogFile == stdout) - fflush(g.LogFile); - else - fclose(g.LogFile); - g.LogFile = NULL; - } - if (g.LogClipboard->size() > 1) - { - SetClipboardText(g.LogClipboard->begin()); - g.LogClipboard->clear(); - } - g.LogEnabled = false; -} + // Add to popup stacks: update OpenPopupStack[] data, push to BeginPopupStack[] + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + popup_ref.Window = window; + popup_ref.ParentNavLayer = parent_window_in_stack->DC.NavLayerCurrent; + g.BeginPopupStack.push_back(popup_ref); + window->PopupId = popup_ref.PopupId; + } -// Helper to display logging buttons -void ImGui::LogButtons() -{ - ImGuiContext &g = *GImGui; - - PushID("LogButtons"); - const bool log_to_tty = Button("Log To TTY"); - SameLine(); - const bool log_to_file = Button("Log To File"); - SameLine(); - const bool log_to_clipboard = Button("Log To Clipboard"); - SameLine(); - PushItemWidth(80.0f); - PushAllowKeyboardFocus(false); - SliderInt("Depth", &g.LogAutoExpandMaxDepth, 0, 9, NULL); - PopAllowKeyboardFocus(); - PopItemWidth(); - PopID(); - - // Start logging at the end of the function so that the buttons don't appear in the log - if (log_to_tty) - LogToTTY(g.LogAutoExpandMaxDepth); - if (log_to_file) - LogToFile(g.LogAutoExpandMaxDepth, g.IO.LogFilename); - if (log_to_clipboard) - LogToClipboard(g.LogAutoExpandMaxDepth); -} - -bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) -{ - if (flags & ImGuiTreeNodeFlags_Leaf) - return true; - - // We only write to the tree storage if the user clicks (or explicitely use SetNextTreeNode*** functions) - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - ImGuiStorage *storage = window->DC.StateStorage; - - bool is_open; - if (g.NextTreeNodeOpenCond != 0) - { - if (g.NextTreeNodeOpenCond & ImGuiCond_Always) - { - is_open = g.NextTreeNodeOpenVal; - storage->SetInt(id, is_open); - } - else - { - // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. - const int stored_value = storage->GetInt(id, -1); - if (stored_value == -1) - { - is_open = g.NextTreeNodeOpenVal; - storage->SetInt(id, is_open); - } - else - { - is_open = stored_value != 0; - } - } - g.NextTreeNodeOpenCond = 0; - } - else - { - is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; - } - - // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). - // NB- If we are above max depth we still allow manually opened nodes to be logged. - if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && window->DC.TreeDepth < g.LogAutoExpandMaxDepth) - is_open = true; - - return is_open; -} - -bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char *label, const char *label_end) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; - const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f); - - if (!label_end) - label_end = FindRenderedTextEnd(label); - const ImVec2 label_size = CalcTextSize(label, label_end, false); - - // We vertically grow up to current line height up the typical widget height. - const float text_base_offset_y = ImMax(padding.y, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it - const float frame_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); - ImRect frame_bb = ImRect(window->DC.CursorPos, ImVec2(window->Pos.x + GetContentRegionMax().x, window->DC.CursorPos.y + frame_height)); - if (display_frame) - { - // Framed header expand a little outside the default padding - frame_bb.Min.x -= (float) (int) (window->WindowPadding.x * 0.5f) - 1; - frame_bb.Max.x += (float) (int) (window->WindowPadding.x * 0.5f) - 1; - } - - const float text_offset_x = (g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2)); // Collapser arrow width + Spacing - const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser - ItemSize(ImVec2(text_width, frame_height), text_base_offset_y); - - // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing - // (Ideally we'd want to add a flag for the user to specify if we want the hit test to be done up to the right side of the content or not) - const ImRect interact_bb = display_frame ? frame_bb : ImRect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + text_width + style.ItemSpacing.x * 2, frame_bb.Max.y); - bool is_open = TreeNodeBehaviorIsOpen(id, flags); - - // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. - // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). - // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. - if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - window->DC.TreeDepthMayJumpToParentOnPop |= (1 << window->DC.TreeDepth); - - bool item_add = ItemAdd(interact_bb, id); - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; - window->DC.LastItemDisplayRect = frame_bb; - - if (!item_add) - { - if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - TreePushRawID(id); - return is_open; - } - - // Flags that affects opening behavior: - // - 0(default) ..................... single-click anywhere to open - // - OpenOnDoubleClick .............. double-click anywhere to open - // - OpenOnArrow .................... single-click on arrow to open - // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open - ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0); - if (!(flags & ImGuiTreeNodeFlags_Leaf)) - button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; - if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) - button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); - - bool hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); - if (!(flags & ImGuiTreeNodeFlags_Leaf)) - { - bool toggled = false; - if (pressed) - { - toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); - if (flags & ImGuiTreeNodeFlags_OpenOnArrow) - toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); - if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) - toggled |= g.IO.MouseDoubleClicked[0]; - if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. - toggled = false; - } - - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open) - { - toggled = true; - NavMoveRequestCancel(); - } - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? - { - toggled = true; - NavMoveRequestCancel(); - } - - if (toggled) - { - is_open = !is_open; - window->DC.StateStorage->SetInt(id, is_open); - } - } - if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) - SetItemAllowOverlap(); - - // Render - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : - ImGuiCol_Header); - const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); - if (display_frame) - { - // Framed type - RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding); - RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); - RenderTriangle(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); - if (g.LogEnabled) - { - // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. - const char log_prefix[] = "\n##"; - const char log_suffix[] = "##"; - LogRenderedText(&text_pos, log_prefix, log_prefix + 3); - RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); - LogRenderedText(&text_pos, log_suffix + 1, log_suffix + 3); - } - else - { - RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); - } - } - else - { - // Unframed typed for tree nodes - if (hovered || (flags & ImGuiTreeNodeFlags_Selected)) - { - RenderFrame(frame_bb.Min, frame_bb.Max, col, false); - RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); - } - - if (flags & ImGuiTreeNodeFlags_Bullet) - RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize * 0.50f + text_base_offset_y)); - else if (!(flags & ImGuiTreeNodeFlags_Leaf)) - RenderTriangle(frame_bb.Min + ImVec2(padding.x, g.FontSize * 0.15f + text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); - if (g.LogEnabled) - LogRenderedText(&text_pos, ">"); - RenderText(text_pos, label, label_end, false); - } - - if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - TreePushRawID(id); - return is_open; -} - -// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). -// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). -bool ImGui::CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen, label); -} - -bool ImGui::CollapsingHeader(const char *label, bool *p_open, ImGuiTreeNodeFlags flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - if (p_open && !*p_open) - return false; - - ImGuiID id = window->GetID(label); - bool is_open = TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap : 0), label); - if (p_open) - { - // Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. - ImGuiContext &g = *GImGui; - float button_sz = g.FontSize * 0.5f; - ImGuiItemHoveredDataBackup last_item_backup; - if (CloseButton(window->GetID((void *) (intptr_t) (id + 1)), ImVec2(ImMin(window->DC.LastItemRect.Max.x, window->ClipRect.Max.x) - g.Style.FramePadding.x - button_sz, window->DC.LastItemRect.Min.y + g.Style.FramePadding.y + button_sz), button_sz)) - *p_open = false; - last_item_backup.Restore(); - } + // Process SetNextWindow***() calls + // (FIXME: Consider splitting the HasXXX flags into X/Y components + bool window_pos_set_by_api = false; + bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) + { + window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; + if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) + { + // May be processed on the next frame if this is our first frame and we are measuring size + // FIXME: Look into removing the branch so everything can go through this same code path for consistency. + window->SetWindowPosVal = g.NextWindowData.PosVal; + window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + } + else + { + SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); + } + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) + { + window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); + window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) // Axis-specific conditions for BeginChild() + g.NextWindowData.SizeVal.x = window->SizeFull.x; + if ((window->ChildFlags & ImGuiChildFlags_ResizeY) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) + g.NextWindowData.SizeVal.y = window->SizeFull.y; + SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasScroll) + { + if (g.NextWindowData.ScrollVal.x >= 0.0f) + { + window->ScrollTarget.x = g.NextWindowData.ScrollVal.x; + window->ScrollTargetCenterRatio.x = 0.0f; + } + if (g.NextWindowData.ScrollVal.y >= 0.0f) + { + window->ScrollTarget.y = g.NextWindowData.ScrollVal.y; + window->ScrollTargetCenterRatio.y = 0.0f; + } + } + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasContentSize) + window->ContentSizeExplicit = g.NextWindowData.ContentSizeVal; + else if (first_begin_of_the_frame) + window->ContentSizeExplicit = ImVec2(0.0f, 0.0f); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasCollapsed) + SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); + if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasFocus) + FocusWindow(window); + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + + // [EXPERIMENTAL] Skip Refresh mode + UpdateWindowSkipRefresh(window); + + // Nested root windows (typically tooltips) override disabled state + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + BeginDisabledOverrideReenable(); + + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() + g.CurrentWindow = NULL; + + // When reusing window again multiple times a frame, just append content (don't need to setup again) + if (first_begin_of_the_frame && !window->SkipRefresh) + { + // Initialize + const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFramesCannotSkipItems > 0); + window->Active = true; + window->HasCloseButton = (p_open != NULL); + window->ClipRect = ImVec4(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); + window->IDStack.resize(1); + window->DrawList->_ResetForNewFrame(); + window->DC.CurrentTableIdx = -1; + + // Restore buffer capacity when woken from a compacted state, to avoid + if (window->MemoryCompacted) + GcAwakeTransientWindowBuffers(window); + + // Update stored window name when it changes (which can _only_ happen with the "###" operator, so the ID would stay unchanged). + // The title bar always display the 'name' parameter, so we only update the string storage if it needs to be visible to the end-user elsewhere. + bool window_title_visible_elsewhere = false; + if (g.NavWindowingListWindow != NULL && (window->Flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + window_title_visible_elsewhere = true; + if (window_title_visible_elsewhere && !window_just_created && strcmp(name, window->Name) != 0) + { + size_t buf_len = (size_t)window->NameBufLen; + window->Name = ImStrdupcpy(window->Name, &buf_len, name); + window->NameBufLen = (int)buf_len; + } + + // UPDATE CONTENTS SIZE, UPDATE HIDDEN STATUS + + // Update contents size from last frame for auto-fitting (or use explicit size) + CalcWindowContentSizes(window, &window->ContentSize, &window->ContentSizeIdeal); + if (window->HiddenFramesCanSkipItems > 0) + window->HiddenFramesCanSkipItems--; + if (window->HiddenFramesCannotSkipItems > 0) + window->HiddenFramesCannotSkipItems--; + if (window->HiddenFramesForRenderOnly > 0) + window->HiddenFramesForRenderOnly--; + + // Hide new windows for one frame until they calculate their size + if (window_just_created && (!window_size_x_set_by_api || !window_size_y_set_by_api)) + window->HiddenFramesCannotSkipItems = 1; + + // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) + // We reset Size/ContentSize for reappearing popups/tooltips early in this function, so further code won't be tempted to use the old size. + if (window_just_activated_by_user && (flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0) + { + window->HiddenFramesCannotSkipItems = 1; + if (flags & ImGuiWindowFlags_AlwaysAutoResize) + { + if (!window_size_x_set_by_api) + window->Size.x = window->SizeFull.x = 0.f; + if (!window_size_y_set_by_api) + window->Size.y = window->SizeFull.y = 0.f; + window->ContentSize = window->ContentSizeIdeal = ImVec2(0.f, 0.f); + } + } + + // SELECT VIEWPORT + // FIXME-VIEWPORT: In the docking/viewport branch, this is the point where we select the current viewport (which may affect the style) + + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + SetWindowViewport(window, viewport); + SetCurrentWindow(window); + + // LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies) + + if (flags & ImGuiWindowFlags_ChildWindow) + window->WindowBorderSize = style.ChildBorderSize; + else + window->WindowBorderSize = ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; + window->WindowPadding = style.WindowPadding; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !(window->ChildFlags & ImGuiChildFlags_AlwaysUseWindowPadding) && window->WindowBorderSize == 0.0f) + window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. + window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); + window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; + window->TitleBarHeight = (flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : g.FontSize + g.Style.FramePadding.y * 2.0f; + window->MenuBarHeight = (flags & ImGuiWindowFlags_MenuBar) ? window->DC.MenuBarOffset.y + g.FontSize + g.Style.FramePadding.y * 2.0f : 0.0f; + + // Depending on condition we use previous or current window size to compare against contents size to decide if a scrollbar should be visible. + // Those flags will be altered further down in the function depending on more conditions. + bool use_current_size_for_scrollbar_x = window_just_created; + bool use_current_size_for_scrollbar_y = window_just_created; + if (window_size_x_set_by_api && window->ContentSizeExplicit.x != 0.0f) + use_current_size_for_scrollbar_x = true; + if (window_size_y_set_by_api && window->ContentSizeExplicit.y != 0.0f) // #7252 + use_current_size_for_scrollbar_y = true; + + // Collapse window by double-clicking on title bar + // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + { + // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), so verify that we don't have items over the title bar. + ImRect title_bar_rect = window->TitleBarRect(); + if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max)) + if (g.IO.MouseClickedCount[0] == 2 && GetKeyOwner(ImGuiKey_MouseLeft) == ImGuiKeyOwner_NoOwner) + window->WantCollapseToggle = true; + if (window->WantCollapseToggle) + { + window->Collapsed = !window->Collapsed; + if (!window->Collapsed) + use_current_size_for_scrollbar_y = true; + MarkIniSettingsDirty(window); + } + } + else + { + window->Collapsed = false; + } + window->WantCollapseToggle = false; + + // SIZE + + // Outer Decoration Sizes + // (we need to clear ScrollbarSize immediately as CalcWindowAutoFitSize() needs it and can be called from other locations). + const ImVec2 scrollbar_sizes_from_last_frame = window->ScrollbarSizes; + window->DecoOuterSizeX1 = 0.0f; + window->DecoOuterSizeX2 = 0.0f; + window->DecoOuterSizeY1 = window->TitleBarHeight + window->MenuBarHeight; + window->DecoOuterSizeY2 = 0.0f; + window->ScrollbarSizes = ImVec2(0.0f, 0.0f); + + // Calculate auto-fit size, handle automatic resize + const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSizeIdeal); + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) && !window->Collapsed) + { + // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. + if (!window_size_x_set_by_api) + { + window->SizeFull.x = size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api) + { + window->SizeFull.y = size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + } + else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + { + // Auto-fit may only grow window during the first few frames + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. + if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) + { + window->SizeFull.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) + { + window->SizeFull.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + if (!window->Collapsed) + MarkIniSettingsDirty(window); + } + + // Apply minimum/maximum window size constraints and final size + window->SizeFull = CalcWindowSizeAfterConstraint(window, window->SizeFull); + window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; + + // POSITION + + // Popup latch its initial position, will position itself when it appears next frame + if (window_just_activated_by_user) + { + window->AutoPosLastDirection = ImGuiDir_None; + if ((flags & ImGuiWindowFlags_Popup) != 0 && !(flags & ImGuiWindowFlags_Modal) && !window_pos_set_by_api) // FIXME: BeginPopup() could use SetNextWindowPos() + window->Pos = g.BeginPopupStack.back().OpenPopupPos; + } + + // Position child window + if (flags & ImGuiWindowFlags_ChildWindow) + { + IM_ASSERT(parent_window && parent_window->Active); + window->BeginOrderWithinParent = (short)parent_window->DC.ChildWindows.Size; + parent_window->DC.ChildWindows.push_back(window); + if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = parent_window->DC.CursorPos; + } + + const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFramesCannotSkipItems == 0); + if (window_pos_with_pivot) + SetWindowPos(window, window->SetWindowPosVal - window->Size * window->SetWindowPosPivot, 0); // Position given a pivot (e.g. for centering) + else if ((flags & ImGuiWindowFlags_ChildMenu) != 0) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = FindBestWindowPosForPopup(window); + + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) + // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. + ImRect viewport_rect(viewport->GetMainRect()); + ImRect viewport_work_rect(viewport->GetWorkRect()); + ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + ImRect visibility_rect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding); + + // Clamp position/size so window stays visible within its viewport or monitor + // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow)) + if (viewport_rect.GetWidth() > 0.0f && viewport_rect.GetHeight() > 0.0f) + ClampWindowPos(window, visibility_rect); + window->Pos = ImTrunc(window->Pos); + + // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) + // Large values tend to lead to variety of artifacts and are not recommended. + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + + // For windows with title bar or menu bar, we clamp to FrameHeight(FontSize + FramePadding.y * 2.0f) to completely hide artifacts. + //if ((window->Flags & ImGuiWindowFlags_MenuBar) || !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + // window->WindowRounding = ImMin(window->WindowRounding, g.FontSize + style.FramePadding.y * 2.0f); + + // Apply window focus (new and reactivated windows are moved to front) + bool want_focus = false; + if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) + { + if (flags & ImGuiWindowFlags_Popup) + want_focus = true; + else if ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) == 0) + want_focus = true; + } + + // [Test Engine] Register whole window in the item system (before submitting further decorations) +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (g.TestEngineHookItems) + { + IM_ASSERT(window->IDStack.Size == 1); + window->IDStack.Size = 0; // As window->IDStack[0] == window->ID here, make sure TestEngine doesn't erroneously see window as parent of itself. + IMGUI_TEST_ENGINE_ITEM_ADD(window->ID, window->Rect(), NULL); + IMGUI_TEST_ENGINE_ITEM_INFO(window->ID, window->Name, (g.HoveredWindow == window) ? ImGuiItemStatusFlags_HoveredRect : 0); + window->IDStack.Size = 1; + } +#endif - return is_open; -} + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_hovered = -1, border_held = -1; + ImU32 resize_grip_col[4] = {}; + const int resize_grip_count = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) ? 0 : g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + if (!window->Collapsed) + if (int auto_fit_mask = UpdateWindowManualResize(window, size_auto_fit, &border_hovered, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) + { + if (auto_fit_mask & (1 << ImGuiAxis_X)) + use_current_size_for_scrollbar_x = true; + if (auto_fit_mask & (1 << ImGuiAxis_Y)) + use_current_size_for_scrollbar_y = true; + } + window->ResizeBorderHovered = (signed char)border_hovered; + window->ResizeBorderHeld = (signed char)border_held; + + // SCROLLBAR VISIBILITY + + // Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size). + if (!window->Collapsed) + { + // When reading the current size we need to read it after size constraints have been applied. + // Intentionally use previous frame values for InnerRect and ScrollbarSizes. + // And when we use window->DecorationUp here it doesn't have ScrollbarSizes.y applied yet. + ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2)); + ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + scrollbar_sizes_from_last_frame; + ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f; + float size_x_for_scrollbars = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x; + float size_y_for_scrollbars = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y; + //bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons? + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + if (window->ScrollbarX && !window->ScrollbarY) + window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + + // Amend the partially filled window->DecorationXXX values. + window->DecoOuterSizeX2 += window->ScrollbarSizes.x; + window->DecoOuterSizeY2 += window->ScrollbarSizes.y; + } + + // UPDATE RECTANGLES (1- THOSE NOT AFFECTED BY SCROLLING) + // Update various regions. Variables they depend on should be set above in this function. + // We set this up after processing the resize grip so that our rectangles doesn't lag by a frame. + + // Outer rectangle + // Not affected by window border size. Used by: + // - FindHoveredWindow() (w/ extra padding when border resize is enabled) + // - Begin() initial clipping rect for drawing window background and borders. + // - Begin() clipping whole child + const ImRect host_rect = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) ? parent_window->ClipRect : viewport_rect; + const ImRect outer_rect = window->Rect(); + const ImRect title_bar_rect = window->TitleBarRect(); + window->OuterRectClipped = outer_rect; + window->OuterRectClipped.ClipWith(host_rect); + + // Inner rectangle + // Not affected by window border size. Used by: + // - InnerClipRect + // - ScrollToRectEx() + // - NavUpdatePageUpPageDown() + // - Scrollbar() + window->InnerRect.Min.x = window->Pos.x + window->DecoOuterSizeX1; + window->InnerRect.Min.y = window->Pos.y + window->DecoOuterSizeY1; + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->DecoOuterSizeX2; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->DecoOuterSizeY2; + + // Inner clipping rectangle. + // - Extend a outside of normal work region up to borders. + // - This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. + // - It also makes clipped items be more noticeable. + // - And is consistent on both axis (prior to 2024/05/03 ClipRect used WindowPadding.x * 0.5f on left and right edge), see #3312 + // - Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + // Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior. + // Affected by window/frame border size. Used by: + // - Begin() initial clip rect + float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); + + // Try to match the fact that our border is drawn centered over the window rectangle, rather than inner. + // This is why we do a *0.5f here. We don't currently even technically support large values for WindowBorderSize, + // see e.g #7887 #7888, but may do after we move the window border to become an inner border (and then we can remove the 0.5f here). + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + window->WindowBorderSize * 0.5f); + window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size * 0.5f); + window->InnerClipRect.Max.x = ImFloor(window->InnerRect.Max.x - window->WindowBorderSize * 0.5f); + window->InnerClipRect.Max.y = ImFloor(window->InnerRect.Max.y - window->WindowBorderSize * 0.5f); + window->InnerClipRect.ClipWithFull(host_rect); + + // Default item width. Make it proportional to window size if window manually resizes + if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) + window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f); + else + window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f); + + // SCROLLING + + // Lock down maximum scrolling + // The value of ScrollMax are ahead from ScrollbarX/ScrollbarY which is intentionally using InnerRect from previous rect in order to accommodate + // for right/bottom aligned items without creating a scrollbar. + window->ScrollMax.x = ImMax(0.0f, window->ContentSize.x + window->WindowPadding.x * 2.0f - window->InnerRect.GetWidth()); + window->ScrollMax.y = ImMax(0.0f, window->ContentSize.y + window->WindowPadding.y * 2.0f - window->InnerRect.GetHeight()); + + // Apply scrolling + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); + window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + window->DecoInnerSizeX1 = window->DecoInnerSizeY1 = 0.0f; + + // DRAWING + + // Setup draw list and outer clipping rectangle + IM_ASSERT(window->DrawList->CmdBuffer.Size == 1 && window->DrawList->CmdBuffer[0].ElemCount == 0); + window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); + PushClipRect(host_rect.Min, host_rect.Max, false); + + // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71) + // When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order. + // FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493) + { + bool render_decorations_in_parent = false; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) + { + // - We test overlap with the previous child window only (testing all would end up being O(log N) not a good investment here) + // - We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping childs + ImGuiWindow* previous_child = parent_window->DC.ChildWindows.Size >= 2 ? parent_window->DC.ChildWindows[parent_window->DC.ChildWindows.Size - 2] : NULL; + bool previous_child_overlapping = previous_child ? previous_child->Rect().Overlaps(window->Rect()) : false; + bool parent_is_empty = (parent_window->DrawList->VtxBuffer.Size == 0); + if (window->DrawList->CmdBuffer.back().ElemCount == 0 && !parent_is_empty && !previous_child_overlapping) + render_decorations_in_parent = true; + } + if (render_decorations_in_parent) + window->DrawList = parent_window->DrawList; + + // Handle title bar, scrollbar, resize grips and resize borders + const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; + const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight); + const bool handle_borders_and_resize_grips = true; // This exists to facilitate merge with 'docking' branch. + RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_count, resize_grip_col, resize_grip_draw_size); + + if (render_decorations_in_parent) + window->DrawList = &window->DrawListInst; + } + + // UPDATE RECTANGLES (2- THOSE AFFECTED BY SCROLLING) + + // Work rectangle. + // Affected by window padding and border size. Used by: + // - Columns() for right-most edge + // - TreeNode(), CollapsingHeader() for right-most edge + // - BeginTabBar() for right-most edge + const bool allow_scrollbar_x = !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar); + const bool allow_scrollbar_y = !(flags & ImGuiWindowFlags_NoScrollbar); + const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); + window->WorkRect.Min.x = ImTrunc(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); + window->WorkRect.Min.y = ImTrunc(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); + window->WorkRect.Max.x = window->WorkRect.Min.x + work_rect_size_x; + window->WorkRect.Max.y = window->WorkRect.Min.y + work_rect_size_y; + window->ParentWorkRect = window->WorkRect; + + // [LEGACY] Content Region + // FIXME-OBSOLETE: window->ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // Unless explicit content size is specified by user, this currently represent the region leading to no scrolling. + // Used by: + // - Mouse wheel scrolling + many other things + window->ContentRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x + window->DecoOuterSizeX1; + window->ContentRegionRect.Min.y = window->Pos.y - window->Scroll.y + window->WindowPadding.y + window->DecoOuterSizeY1; + window->ContentRegionRect.Max.x = window->ContentRegionRect.Min.x + (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : (window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + window->ContentRegionRect.Max.y = window->ContentRegionRect.Min.y + (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : (window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); + + // Setup drawing context + // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) + window->DC.Indent.x = window->DecoOuterSizeX1 + window->WindowPadding.x - window->Scroll.x; + window->DC.GroupOffset.x = 0.0f; + window->DC.ColumnsOffset.x = 0.0f; + + // Record the loss of precision of CursorStartPos which can happen due to really large scrolling amount. + // This is used by clipper to compensate and fix the most common use case of large scroll area. Easy and cheap, next best thing compared to switching everything to double or ImU64. + double start_pos_highp_x = (double)window->Pos.x + window->WindowPadding.x - (double)window->Scroll.x + window->DecoOuterSizeX1 + window->DC.ColumnsOffset.x; + double start_pos_highp_y = (double)window->Pos.y + window->WindowPadding.y - (double)window->Scroll.y + window->DecoOuterSizeY1; + window->DC.CursorStartPos = ImVec2((float)start_pos_highp_x, (float)start_pos_highp_y); + window->DC.CursorStartPosLossyness = ImVec2((float)(start_pos_highp_x - window->DC.CursorStartPos.x), (float)(start_pos_highp_y - window->DC.CursorStartPos.y)); + window->DC.CursorPos = window->DC.CursorStartPos; + window->DC.CursorPosPrevLine = window->DC.CursorPos; + window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.IdealMaxPos = window->DC.CursorStartPos; + window->DC.CurrLineSize = window->DC.PrevLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.NavLayersActiveMask = window->DC.NavLayersActiveMaskNext; + window->DC.NavLayersActiveMaskNext = 0x00; + window->DC.NavIsScrollPushableX = true; + window->DC.NavHideHighlightOneFrame = false; + window->DC.NavWindowHasScrollY = (window->ScrollMax.y > 0.0f); + + window->DC.MenuBarAppending = false; + window->DC.MenuColumns.Update(style.ItemSpacing.x, window_just_activated_by_user); + window->DC.TreeDepth = 0; + window->DC.TreeHasStackDataDepthMask = 0x00; + window->DC.ChildWindows.resize(0); + window->DC.StateStorage = &window->StateStorage; + window->DC.CurrentColumns = NULL; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; + + window->DC.ItemWidth = window->ItemWidthDefault; + window->DC.TextWrapPos = -1.0f; // disabled + window->DC.ItemWidthStack.resize(0); + window->DC.TextWrapPosStack.resize(0); + if (flags & ImGuiWindowFlags_Modal) + window->DC.ModalDimBgColor = ColorConvertFloat4ToU32(GetStyleColorVec4(ImGuiCol_ModalWindowDimBg)); + + if (window->AutoFitFramesX > 0) + window->AutoFitFramesX--; + if (window->AutoFitFramesY > 0) + window->AutoFitFramesY--; + + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + // We ImGuiFocusRequestFlags_UnlessBelowModal to: + // - Avoid focusing a window that is created outside of a modal. This will prevent active modal from being closed. + // - Position window behind the modal that is not a begin-parent of this window. + if (want_focus) + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + if (want_focus && window == g.NavWindow) + NavInitWindow(window, false); // <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window->WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window->WindowBorderSize, title_bar_rect.Max.y), name, p_open); + + // Clear hit test shape every frame + window->HitTestHoleSize.x = window->HitTestHoleSize.y = 0; + + // Pressing CTRL+C while holding on a window copy its content to the clipboard + // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. + // Maybe we can support CTRL+C on every element? + /* + //if (g.NavWindow == window && g.ActiveId == 0) + if (g.ActiveId == window->MoveId) + if (g.IO.KeyCtrl && IsKeyPressed(ImGuiKey_C)) + LogToClipboard(); + */ + + // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). + // This is useful to allow creating context menus on title bar only, etc. + SetLastItemDataForWindow(window, title_bar_rect); + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugLocateId != 0 && (window->ID == g.DebugLocateId || window->MoveId == g.DebugLocateId)) + DebugLocateItemResolveWithLastItem(); +#endif -bool ImGui::TreeNodeEx(const char *label, ImGuiTreeNodeFlags flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + // [Test Engine] Register title bar / tab with MoveId. +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (!(window->Flags & ImGuiWindowFlags_NoTitleBar)) + IMGUI_TEST_ENGINE_ITEM_ADD(g.LastItemData.ID, g.LastItemData.Rect, &g.LastItemData); +#endif + } + else + { + // Skip refresh always mark active + if (window->SkipRefresh) + SetWindowActiveForSkipRefresh(window); + + // Append + SetCurrentWindow(window); + SetLastItemDataForWindow(window, window->TitleBarRect()); + } + + if (!window->SkipRefresh) + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + + // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) + window->WriteAccessed = false; + window->BeginCount++; + g.NextWindowData.ClearFlags(); + + // Update visibility + if (first_begin_of_the_frame && !window->SkipRefresh) + { + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_ChildMenu)) + { + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); + const bool nav_request = (window->ChildFlags & ImGuiChildFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (!g.LogEnabled && !nav_request) + if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + { + if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + window->HiddenFramesCannotSkipItems = 1; + else + window->HiddenFramesCanSkipItems = 1; + } + + // Hide along with parent or if parent is collapsed + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) + window->HiddenFramesCanSkipItems = 1; + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCannotSkipItems > 0)) + window->HiddenFramesCannotSkipItems = 1; + } + + // Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point) + if (style.Alpha <= 0.0f) + window->HiddenFramesCanSkipItems = 1; + + // Update the Hidden flag + bool hidden_regular = (window->HiddenFramesCanSkipItems > 0) || (window->HiddenFramesCannotSkipItems > 0); + window->Hidden = hidden_regular || (window->HiddenFramesForRenderOnly > 0); + + // Disable inputs for requested number of frames + if (window->DisableInputsFrames > 0) + { + window->DisableInputsFrames--; + window->Flags |= ImGuiWindowFlags_NoInputs; + } + + // Update the SkipItems flag, used to early out of all items functions (no layout required) + bool skip_items = false; + if (window->Collapsed || !window->Active || hidden_regular) + if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) + skip_items = true; + window->SkipItems = skip_items; + } + else if (first_begin_of_the_frame) + { + // Skip refresh mode + window->SkipItems = true; + } + + // [DEBUG] io.ConfigDebugBeginReturnValue override return value to test Begin/End and BeginChild/EndChild behaviors. + // (The implicit fallback window is NOT automatically ended allowing it to always be able to receive commands without crashing) +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (!window->IsFallbackWindow) + if ((g.IO.ConfigDebugBeginReturnValueOnce && window_just_created) || (g.IO.ConfigDebugBeginReturnValueLoop && g.DebugBeginReturnValueCullDepth == g.CurrentWindowStack.Size)) + { + if (window->AutoFitFramesX > 0) { window->AutoFitFramesX++; } + if (window->AutoFitFramesY > 0) { window->AutoFitFramesY++; } + return false; + } +#endif - return TreeNodeBehavior(window->GetID(label), flags, label, NULL); + return !window->SkipItems; } -bool ImGui::TreeNodeExV(const char *str_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) +static void ImGui::SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const char *label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end); + ImGuiContext& g = *GImGui; + SetLastItemData(window->MoveId, g.CurrentItemFlags, IsMouseHoveringRect(rect.Min, rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0, rect); } -bool ImGui::TreeNodeExV(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) +void ImGui::End() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Error checking: verify that user hasn't called End() too many times! + if (g.CurrentWindowStack.Size <= 1 && g.WithinFrameScopeWithImplicitWindow) + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size > 1, "Calling End() too many times!"); + return; + } + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); + + // Error checking: verify that user doesn't directly call End() on a child window. + if (window->Flags & ImGuiWindowFlags_ChildWindow) + IM_ASSERT_USER_ERROR(g.WithinEndChild, "Must call EndChild() and not End()!"); + + // Close anything that is open + if (window->DC.CurrentColumns) + EndColumns(); + if (!window->SkipRefresh) + PopClipRect(); // Inner window clip rectangle + PopFocusScope(); + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + EndDisabledOverrideReenable(); + + if (window->SkipRefresh) + { + IM_ASSERT(window->DrawList == NULL); + window->DrawList = &window->DrawListInst; + } + + // Stop logging + if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging + LogFinish(); - ImGuiContext &g = *GImGui; - const char *label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end); + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Pop from window stack + g.LastItemData = window_stack_data.ParentLastItemDataBackup; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth--; + if (window->Flags & ImGuiWindowFlags_Popup) + g.BeginPopupStack.pop_back(); + window_stack_data.StackSizesOnBegin.CompareWithContextState(&g); + g.CurrentWindowStack.pop_back(); + SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); } -bool ImGui::TreeNodeV(const char *str_id, const char *fmt, va_list args) +void ImGui::BringWindowToFocusFront(ImGuiWindow* window) { - return TreeNodeExV(str_id, 0, fmt, args); + ImGuiContext& g = *GImGui; + IM_ASSERT(window == window->RootWindow); + + const int cur_order = window->FocusOrder; + IM_ASSERT(g.WindowsFocusOrder[cur_order] == window); + if (g.WindowsFocusOrder.back() == window) + return; + + const int new_order = g.WindowsFocusOrder.Size - 1; + for (int n = cur_order; n < new_order; n++) + { + g.WindowsFocusOrder[n] = g.WindowsFocusOrder[n + 1]; + g.WindowsFocusOrder[n]->FocusOrder--; + IM_ASSERT(g.WindowsFocusOrder[n]->FocusOrder == n); + } + g.WindowsFocusOrder[new_order] = window; + window->FocusOrder = (short)new_order; +} + +void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindow == window) // Cheap early out (could be better) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window + if (g.Windows[i] == window) + { + memmove(&g.Windows[i], &g.Windows[i + 1], (size_t)(g.Windows.Size - i - 1) * sizeof(ImGuiWindow*)); + g.Windows[g.Windows.Size - 1] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* behind_window) +{ + IM_ASSERT(window != NULL && behind_window != NULL); + ImGuiContext& g = *GImGui; + window = window->RootWindow; + behind_window = behind_window->RootWindow; + int pos_wnd = FindWindowDisplayIndex(window); + int pos_beh = FindWindowDisplayIndex(behind_window); + if (pos_wnd < pos_beh) + { + size_t copy_bytes = (pos_beh - pos_wnd - 1) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_wnd], &g.Windows.Data[pos_wnd + 1], copy_bytes); + g.Windows[pos_beh - 1] = window; + } + else + { + size_t copy_bytes = (pos_wnd - pos_beh) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_beh + 1], &g.Windows.Data[pos_beh], copy_bytes); + g.Windows[pos_beh] = window; + } } -bool ImGui::TreeNodeV(const void *ptr_id, const char *fmt, va_list args) +int ImGui::FindWindowDisplayIndex(ImGuiWindow* window) { - return TreeNodeExV(ptr_id, 0, fmt, args); + ImGuiContext& g = *GImGui; + return g.Windows.index_from_ptr(g.Windows.find(window)); } -bool ImGui::TreeNodeEx(const char *str_id, ImGuiTreeNodeFlags flags, const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(str_id, flags, fmt, args); - va_end(args); - return is_open; +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags) +{ + ImGuiContext& g = *GImGui; + + // Modal check? + if ((flags & ImGuiFocusRequestFlags_UnlessBelowModal) && (g.NavWindow != window)) // Early out in common case. + if (ImGuiWindow* blocking_modal = FindBlockingModal(window)) + { + // This block would typically be reached in two situations: + // - API call to FocusWindow() with a window under a modal and ImGuiFocusRequestFlags_UnlessBelowModal flag. + // - User clicking on void or anything behind a modal while a modal is open (window == NULL) + IMGUI_DEBUG_LOG_FOCUS("[focus] FocusWindow(\"%s\", UnlessBelowModal): prevented by \"%s\".\n", window ? window->Name : "", blocking_modal->Name); + if (window && window == window->RootWindow && (window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayBehind(window, blocking_modal); // Still bring right under modal. (FIXME: Could move in focus list too?) + ClosePopupsOverWindow(GetTopMostPopupModal(), false); // Note how we need to use GetTopMostPopupModal() aad NOT blocking_modal, to handle nested modals + return; + } + + // Find last focused child (if any) and focus it instead. + if ((flags & ImGuiFocusRequestFlags_RestoreFocusedChild) && window != NULL) + window = NavRestoreLastChildNavWindow(window); + + // Apply focus + if (g.NavWindow != window) + { + SetNavWindow(window); + if (window && g.NavDisableMouseHover) + g.NavMousePosDirty = true; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavLayer = ImGuiNavLayer_Main; + SetNavFocusScope(window ? window->NavRootFocusScopeId : 0); + g.NavIdIsAlive = false; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + + // Close popups if any + ClosePopupsOverWindow(window, false); + } + + // Move the root window to the top of the pile + IM_ASSERT(window == NULL || window->RootWindow != NULL); + ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; // NB: In docking branch this is window->RootWindowDockStop + ImGuiWindow* display_front_window = window ? window->RootWindow : NULL; + + // Steal active widgets. Some of the cases it triggers includes: + // - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run. + // - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId) + if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != focus_front_window) + if (!g.ActiveIdNoClearOnFocusLoss) + ClearActiveID(); + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + + // Bring to front + BringWindowToFocusFront(focus_front_window); + if (((window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayFront(display_front_window); } -bool ImGui::TreeNodeEx(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, ...) +void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); - va_end(args); - return is_open; + ImGuiContext& g = *GImGui; + IM_UNUSED(filter_viewport); // Unused in master branch. + int start_idx = g.WindowsFocusOrder.Size - 1; + if (under_this_window != NULL) + { + // Aim at root window behind us, if we are in a child window that's our own root (see #4640) + int offset = -1; + while (under_this_window->Flags & ImGuiWindowFlags_ChildWindow) + { + under_this_window = under_this_window->ParentWindow; + offset = 0; + } + start_idx = FindWindowFocusIndex(under_this_window) + offset; + } + for (int i = start_idx; i >= 0; i--) + { + // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. + ImGuiWindow* window = g.WindowsFocusOrder[i]; + if (window == ignore_window || !window->WasActive) + continue; + if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) + { + FocusWindow(window, flags); + return; + } + } + FocusWindow(NULL, flags); } -bool ImGui::TreeNode(const char *str_id, const char *fmt, ...) +// Important: this alone doesn't alter current ImDrawList state. This is called by PushFont/PopFont only. +void ImGui::SetCurrentFont(ImFont* font) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(str_id, 0, fmt, args); - va_end(args); - return is_open; + ImGuiContext& g = *GImGui; + IM_ASSERT(font && font->IsLoaded()); // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ? + IM_ASSERT(font->Scale > 0.0f); + g.Font = font; + g.FontBaseSize = ImMax(1.0f, g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale); + g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; + g.FontScale = g.FontSize / g.Font->FontSize; + + ImFontAtlas* atlas = g.Font->ContainerAtlas; + g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.TexUvLines = atlas->TexUvLines; + g.DrawListSharedData.Font = g.Font; + g.DrawListSharedData.FontSize = g.FontSize; + g.DrawListSharedData.FontScale = g.FontScale; } -bool ImGui::TreeNode(const void *ptr_id, const char *fmt, ...) +// Use ImDrawList::_SetTextureID(), making our shared g.FontStack[] authorative against window-local ImDrawList. +// - Whereas ImDrawList::PushTextureID()/PopTextureID() is not to be used across Begin() calls. +// - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did... +// - Some code paths never really fully worked with multiple atlas textures. +// - The right-ish solution may be to remove _SetTextureID() and make AddText/RenderText lazily call PushTextureID()/PopTextureID() +// the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem +// because we have a concrete need and a test bed for multiple atlas textures. +void ImGui::PushFont(ImFont* font) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); - va_end(args); - return is_open; + ImGuiContext& g = *GImGui; + if (font == NULL) + font = GetDefaultFont(); + g.FontStack.push_back(font); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); } -bool ImGui::TreeNode(const char *label) +void ImGui::PopFont() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - return TreeNodeBehavior(window->GetID(label), 0, label, NULL); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.FontStack.Size > 0); + g.FontStack.pop_back(); + ImFont* font = g.FontStack.Size == 0 ? GetDefaultFont() : g.FontStack.back(); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); } -void ImGui::TreeAdvanceToLabelPos() +void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) { - ImGuiContext &g = *GImGui; - g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing(); + ImGuiContext& g = *GImGui; + ImGuiItemFlags item_flags = g.CurrentItemFlags; + IM_ASSERT(item_flags == g.ItemFlagsStack.back()); + if (enabled) + item_flags |= option; + else + item_flags &= ~option; + g.CurrentItemFlags = item_flags; + g.ItemFlagsStack.push_back(item_flags); } -// Horizontal distance preceding label when using TreeNode() or Bullet() -float ImGui::GetTreeNodeToLabelSpacing() +void ImGui::PopItemFlag() { - ImGuiContext &g = *GImGui; - return g.FontSize + (g.Style.FramePadding.x * 2.0f); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ItemFlagsStack.Size > 1); // Too many calls to PopItemFlag() - we always leave a 0 at the bottom of the stack. + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); } -void ImGui::SetNextTreeNodeOpen(bool is_open, ImGuiCond cond) +// BeginDisabled()/EndDisabled() +// - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) +// - Visually this is currently altering alpha, but it is expected that in a future styling system this would work differently. +// - Feedback welcome at https://github.com/ocornut/imgui/issues/211 +// - BeginDisabled(false) essentially does nothing useful but is provided to facilitate use of boolean expressions. If you can avoid calling BeginDisabled(False)/EndDisabled() best to avoid it. +// - Optimized shortcuts instead of PushStyleVar() + PushItemFlag() +void ImGui::BeginDisabled(bool disabled) { - ImGuiContext &g = *GImGui; - if (g.CurrentWindow->SkipItems) - return; - g.NextTreeNodeOpenVal = is_open; - g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always; + ImGuiContext& g = *GImGui; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (!was_disabled && disabled) + { + g.DisabledAlphaBackup = g.Style.Alpha; + g.Style.Alpha *= g.Style.DisabledAlpha; // PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * g.Style.DisabledAlpha); + } + if (was_disabled || disabled) + g.CurrentItemFlags |= ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); // FIXME-OPT: can we simply skip this and use DisabledStackSize? + g.DisabledStackSize++; } -void ImGui::PushID(const char *str_id) +void ImGui::EndDisabled() { - ImGuiWindow *window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(str_id)); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DisabledStackSize > 0); + g.DisabledStackSize--; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + //PopItemFlag(); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + if (was_disabled && (g.CurrentItemFlags & ImGuiItemFlags_Disabled) == 0) + g.Style.Alpha = g.DisabledAlphaBackup; //PopStyleVar(); } -void ImGui::PushID(const char *str_id_begin, const char *str_id_end) +// Could have been called BeginDisabledDisable() but it didn't want to be award nominated for most awkward function name. +// Ideally we would use a shared e.g. BeginDisabled()->BeginDisabledEx() but earlier needs to be optimal. +// The whole code for this is awkward, will reevaluate if we find a way to implement SetNextItemDisabled(). +void ImGui::BeginDisabledOverrideReenable() { - ImGuiWindow *window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(str_id_begin, str_id_end)); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentItemFlags & ImGuiItemFlags_Disabled); + g.Style.Alpha = g.DisabledAlphaBackup; + g.CurrentItemFlags &= ~ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); + g.DisabledStackSize++; } -void ImGui::PushID(const void *ptr_id) +void ImGui::EndDisabledOverrideReenable() { - ImGuiWindow *window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(ptr_id)); + ImGuiContext& g = *GImGui; + g.DisabledStackSize--; + IM_ASSERT(g.DisabledStackSize > 0); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.Style.Alpha = g.DisabledAlphaBackup * g.Style.DisabledAlpha; } -void ImGui::PushID(int int_id) +void ImGui::PushTextWrapPos(float wrap_pos_x) { - const void *ptr_id = (void *) (intptr_t) int_id; - ImGuiWindow *window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(ptr_id)); + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPosStack.push_back(window->DC.TextWrapPos); + window->DC.TextWrapPos = wrap_pos_x; } -void ImGui::PopID() +void ImGui::PopTextWrapPos() { - ImGuiWindow *window = GetCurrentWindowRead(); - window->IDStack.pop_back(); + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPos = window->DC.TextWrapPosStack.back(); + window->DC.TextWrapPosStack.pop_back(); } -ImGuiID ImGui::GetID(const char *str_id) +static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy) { - return GImGui->CurrentWindow->GetID(str_id); + ImGuiWindow* last_window = NULL; + while (last_window != window) + { + last_window = window; + window = window->RootWindow; + if (popup_hierarchy) + window = window->RootWindowPopupTree; + } + return window; } -ImGuiID ImGui::GetID(const char *str_id_begin, const char *str_id_end) +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy) { - return GImGui->CurrentWindow->GetID(str_id_begin, str_id_end); + ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy); + if (window_root == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + if (window == window_root) // end of chain + return false; + window = window->ParentWindow; + } + return false; } -ImGuiID ImGui::GetID(const void *ptr_id) +bool ImGui::IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent) { - return GImGui->CurrentWindow->GetID(ptr_id); + if (window->RootWindow == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + window = window->ParentWindowInBeginStack; + } + return false; } -void ImGui::Bullet() +bool ImGui::IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiContext& g = *GImGui; - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const float line_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y * 2), g.FontSize); - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); - ItemSize(bb); - if (!ItemAdd(bb, 0)) - { - SameLine(0, style.FramePadding.x * 2); - return; - } + // It would be saner to ensure that display layer is always reflected in the g.Windows[] order, which would likely requires altering all manipulations of that array + const int display_layer_delta = GetWindowDisplayLayer(potential_above) - GetWindowDisplayLayer(potential_below); + if (display_layer_delta != 0) + return display_layer_delta > 0; - // Render and stay on same line - RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f)); - SameLine(0, style.FramePadding.x * 2); + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* candidate_window = g.Windows[i]; + if (candidate_window == potential_above) + return true; + if (candidate_window == potential_below) + return false; + } + return false; } -// Text with a little bullet aligned to the typical tree node. -void ImGui::BulletTextV(const char *fmt, va_list args) +// Is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. +bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + IM_ASSERT((flags & ~ImGuiHoveredFlags_AllowedMaskForIsWindowHovered) == 0 && "Invalid flags for IsWindowHovered()!"); + + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.HoveredWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + if (ref_window == NULL) + return false; - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; + if ((flags & ImGuiHoveredFlags_AnyWindow) == 0) + { + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiHoveredFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + + bool result; + if (flags & ImGuiHoveredFlags_ChildWindows) + result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + result = (ref_window == cur_window); + if (!result) + return false; + } - const char *text_begin = g.TempBuffer; - const char *text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); - const float text_base_offset_y = ImMax(0.0f, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it - const float line_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y * 2), g.FontSize); - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), ImMax(line_height, label_size.y))); // Empty text doesn't add padding - ItemSize(bb); - if (!ItemAdd(bb, 0)) - return; - - // Render - RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f)); - RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, text_base_offset_y), text_begin, text_end, false); -} - -void ImGui::BulletText(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - BulletTextV(fmt, args); - va_end(args); -} - -static inline void DataTypeFormatString(ImGuiDataType data_type, void *data_ptr, const char *display_format, char *buf, int buf_size) -{ - if (data_type == ImGuiDataType_Int) - ImFormatString(buf, buf_size, display_format, *(int *) data_ptr); - else if (data_type == ImGuiDataType_Float) - ImFormatString(buf, buf_size, display_format, *(float *) data_ptr); -} - -static inline void DataTypeFormatString(ImGuiDataType data_type, void *data_ptr, int decimal_precision, char *buf, int buf_size) -{ - if (data_type == ImGuiDataType_Int) - { - if (decimal_precision < 0) - ImFormatString(buf, buf_size, "%d", *(int *) data_ptr); - else - ImFormatString(buf, buf_size, "%.*d", decimal_precision, *(int *) data_ptr); - } - else if (data_type == ImGuiDataType_Float) - { - if (decimal_precision < 0) - ImFormatString(buf, buf_size, "%f", *(float *) data_ptr); // Ideally we'd have a minimum decimal precision of 1 to visually denote that it is a float, while hiding non-significant digits? - else - ImFormatString(buf, buf_size, "%.*f", decimal_precision, *(float *) data_ptr); - } -} - -static void DataTypeApplyOp(ImGuiDataType data_type, int op, void *value1, const void *value2) // Store into value1 -{ - if (data_type == ImGuiDataType_Int) - { - if (op == '+') - *(int *) value1 = *(int *) value1 + *(const int *) value2; - else if (op == '-') - *(int *) value1 = *(int *) value1 - *(const int *) value2; - } - else if (data_type == ImGuiDataType_Float) - { - if (op == '+') - *(float *) value1 = *(float *) value1 + *(const float *) value2; - else if (op == '-') - *(float *) value1 = *(float *) value1 - *(const float *) value2; - } -} - -// User can input math operators (e.g. +100) to edit a numerical values. -static bool DataTypeApplyOpFromText(const char *buf, const char *initial_value_buf, ImGuiDataType data_type, void *data_ptr, const char *scalar_format) -{ - while (ImCharIsSpace(*buf)) - buf++; - - // We don't support '-' op because it would conflict with inputing negative value. - // Instead you can use +-100 to subtract from an existing value - char op = buf[0]; - if (op == '+' || op == '*' || op == '/') - { - buf++; - while (ImCharIsSpace(*buf)) - buf++; - } - else - { - op = 0; - } - if (!buf[0]) - return false; - - if (data_type == ImGuiDataType_Int) - { - if (!scalar_format) - scalar_format = "%d"; - int *v = (int *) data_ptr; - const int old_v = *v; - int arg0i = *v; - if (op && sscanf(initial_value_buf, scalar_format, &arg0i) < 1) - return false; - - // Store operand in a float so we can use fractional value for multipliers (*1.1), but constant always parsed as integer so we can fit big integers (e.g. 2000000003) past float precision - float arg1f = 0.0f; - if (op == '+') - { - if (sscanf(buf, "%f", &arg1f) == 1) - *v = (int) (arg0i + arg1f); - } // Add (use "+-" to subtract) - else if (op == '*') - { - if (sscanf(buf, "%f", &arg1f) == 1) - *v = (int) (arg0i * arg1f); - } // Multiply - else if (op == '/') - { - if (sscanf(buf, "%f", &arg1f) == 1 && arg1f != 0.0f) - *v = (int) (arg0i / arg1f); - } // Divide - else - { - if (sscanf(buf, scalar_format, &arg0i) == 1) - *v = arg0i; - } // Assign constant (read as integer so big values are not lossy) - return (old_v != *v); - } - else if (data_type == ImGuiDataType_Float) - { - // For floats we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in - scalar_format = "%f"; - float *v = (float *) data_ptr; - const float old_v = *v; - float arg0f = *v; - if (op && sscanf(initial_value_buf, scalar_format, &arg0f) < 1) - return false; - - float arg1f = 0.0f; - if (sscanf(buf, scalar_format, &arg1f) < 1) - return false; - if (op == '+') - { - *v = arg0f + arg1f; - } // Add (use "+-" to subtract) - else if (op == '*') - { - *v = arg0f * arg1f; - } // Multiply - else if (op == '/') - { - if (arg1f != 0.0f) - *v = arg0f / arg1f; - } // Divide - else - { - *v = arg1f; - } // Assign constant - return (old_v != *v); - } - - return false; -} - -// Create text input in place of a slider (when CTRL+Clicking on slider) -// FIXME: Logic is messy and confusing. -bool ImGui::InputScalarAsWidgetReplacement(const ImRect &aabb, const char *label, ImGuiDataType data_type, void *data_ptr, ImGuiID id, int decimal_precision) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - - // Our replacement widget will override the focus ID (registered previously to allow for a TAB focus to happen) - // On the first frame, g.ScalarAsInputTextId == 0, then on subsequent frames it becomes == id - SetActiveID(g.ScalarAsInputTextId, window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - SetHoveredID(0); - FocusableItemUnregister(window); - - char buf[32]; - DataTypeFormatString(data_type, data_ptr, decimal_precision, buf, IM_ARRAYSIZE(buf)); - bool text_value_changed = InputTextEx(label, buf, IM_ARRAYSIZE(buf), aabb.GetSize(), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll); - if (g.ScalarAsInputTextId == 0) // First frame we started displaying the InputText widget - { - IM_ASSERT(g.ActiveId == id); // InputText ID expected to match the Slider ID (else we'd need to store them both, which is also possible) - g.ScalarAsInputTextId = g.ActiveId; - SetHoveredID(id); - } - if (text_value_changed) - return DataTypeApplyOpFromText(buf, GImGui->InputTextState.InitialText.begin(), data_type, data_ptr, NULL); - return false; -} - -// Parse display precision back from the display format string -int ImGui::ParseFormatPrecision(const char *fmt, int default_precision) -{ - int precision = default_precision; - while ((fmt = strchr(fmt, '%')) != NULL) - { - fmt++; - if (fmt[0] == '%') - { - fmt++; - continue; - } // Ignore "%%" - while (*fmt >= '0' && *fmt <= '9') - fmt++; - if (*fmt == '.') - { - fmt = ImAtoi(fmt + 1, &precision); - if (precision < 0 || precision > 10) - precision = default_precision; - } - if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation - precision = -1; - break; - } - return precision; -} - -static float GetMinimumStepAtDecimalPrecision(int decimal_precision) -{ - static const float min_steps[10] = {1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f}; - return (decimal_precision >= 0 && decimal_precision < 10) ? min_steps[decimal_precision] : powf(10.0f, (float) -decimal_precision); -} - -float ImGui::RoundScalar(float value, int decimal_precision) -{ - // Round past decimal precision - // So when our value is 1.99999 with a precision of 0.001 we'll end up rounding to 2.0 - // FIXME: Investigate better rounding methods - if (decimal_precision < 0) - return value; - const float min_step = GetMinimumStepAtDecimalPrecision(decimal_precision); - bool negative = value < 0.0f; - value = fabsf(value); - float remainder = fmodf(value, min_step); - if (remainder <= min_step * 0.5f) - value -= remainder; - else - value += (min_step - remainder); - return negative ? -value : value; -} - -static inline float SliderBehaviorCalcRatioFromValue(float v, float v_min, float v_max, float power, float linear_zero_pos) -{ - if (v_min == v_max) - return 0.0f; - - const bool is_non_linear = (power < 1.0f - 0.00001f) || (power > 1.0f + 0.00001f); - const float v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); - if (is_non_linear) - { - if (v_clamped < 0.0f) - { - const float f = 1.0f - (v_clamped - v_min) / (ImMin(0.0f, v_max) - v_min); - return (1.0f - powf(f, 1.0f / power)) * linear_zero_pos; - } - else - { - const float f = (v_clamped - ImMax(0.0f, v_min)) / (v_max - ImMax(0.0f, v_min)); - return linear_zero_pos + powf(f, 1.0f / power) * (1.0f - linear_zero_pos); - } - } - - // Linear slider - return (v_clamped - v_min) / (v_max - v_min); -} - -bool ImGui::SliderBehavior(const ImRect &frame_bb, ImGuiID id, float *v, float v_min, float v_max, float power, int decimal_precision, ImGuiSliderFlags flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - const ImGuiStyle &style = g.Style; - - // Draw frame - const ImU32 frame_col = GetColorU32((g.ActiveId == id && g.ActiveIdSource == ImGuiInputSource_Nav) ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBg); - RenderNavHighlight(frame_bb, id); - RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); - - const bool is_non_linear = (power < 1.0f - 0.00001f) || (power > 1.0f + 0.00001f); - const bool is_horizontal = (flags & ImGuiSliderFlags_Vertical) == 0; - - const float grab_padding = 2.0f; - const float slider_sz = is_horizontal ? (frame_bb.GetWidth() - grab_padding * 2.0f) : (frame_bb.GetHeight() - grab_padding * 2.0f); - float grab_sz; - if (decimal_precision != 0) - grab_sz = ImMin(style.GrabMinSize, slider_sz); - else - grab_sz = ImMin(ImMax(1.0f * (slider_sz / ((v_min < v_max ? v_max - v_min : v_min - v_max) + 1.0f)), style.GrabMinSize), slider_sz); // Integer sliders, if possible have the grab size represent 1 unit - const float slider_usable_sz = slider_sz - grab_sz; - const float slider_usable_pos_min = (is_horizontal ? frame_bb.Min.x : frame_bb.Min.y) + grab_padding + grab_sz * 0.5f; - const float slider_usable_pos_max = (is_horizontal ? frame_bb.Max.x : frame_bb.Max.y) - grab_padding - grab_sz * 0.5f; - - // For logarithmic sliders that cross over sign boundary we want the exponential increase to be symmetric around 0.0f - float linear_zero_pos = 0.0f; // 0.0->1.0f - if (v_min * v_max < 0.0f) - { - // Different sign - const float linear_dist_min_to_0 = powf(fabsf(0.0f - v_min), 1.0f / power); - const float linear_dist_max_to_0 = powf(fabsf(v_max - 0.0f), 1.0f / power); - linear_zero_pos = linear_dist_min_to_0 / (linear_dist_min_to_0 + linear_dist_max_to_0); - } - else - { - // Same sign - linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f; - } - - // Process interacting with the slider - bool value_changed = false; - if (g.ActiveId == id) - { - bool set_new_value = false; - float clicked_t = 0.0f; - if (g.ActiveIdSource == ImGuiInputSource_Mouse) - { - if (!g.IO.MouseDown[0]) - { - ClearActiveID(); - } - else - { - const float mouse_abs_pos = is_horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; - clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f; - if (!is_horizontal) - clicked_t = 1.0f - clicked_t; - set_new_value = true; - } - } - else if (g.ActiveIdSource == ImGuiInputSource_Nav) - { - const ImVec2 delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f); - float delta = is_horizontal ? delta2.x : -delta2.y; - if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) - { - ClearActiveID(); - } - else if (delta != 0.0f) - { - clicked_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); - if (decimal_precision == 0 && !is_non_linear) - { - if (fabsf(v_max - v_min) <= 100.0f || IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta = ((delta < 0.0f) ? -1.0f : +1.0f) / (v_max - v_min); // Gamepad/keyboard tweak speeds in integer steps - else - delta /= 100.0f; - } - else - { - delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds - if (IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta /= 10.0f; - } - if (IsNavInputDown(ImGuiNavInput_TweakFast)) - delta *= 10.0f; - set_new_value = true; - if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits - set_new_value = false; - else - clicked_t = ImSaturate(clicked_t + delta); - } - } - - if (set_new_value) - { - float new_value; - if (is_non_linear) - { - // Account for logarithmic scale on both sides of the zero - if (clicked_t < linear_zero_pos) - { - // Negative: rescale to the negative range before powering - float a = 1.0f - (clicked_t / linear_zero_pos); - a = powf(a, power); - new_value = ImLerp(ImMin(v_max, 0.0f), v_min, a); - } - else - { - // Positive: rescale to the positive range before powering - float a; - if (fabsf(linear_zero_pos - 1.0f) > 1.e-6f) - a = (clicked_t - linear_zero_pos) / (1.0f - linear_zero_pos); - else - a = clicked_t; - a = powf(a, power); - new_value = ImLerp(ImMax(v_min, 0.0f), v_max, a); - } - } - else - { - // Linear slider - new_value = ImLerp(v_min, v_max, clicked_t); - } - - // Round past decimal precision - new_value = RoundScalar(new_value, decimal_precision); - if (*v != new_value) - { - *v = new_value; - value_changed = true; - } - } - } - - // Draw - float grab_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); - if (!is_horizontal) - grab_t = 1.0f - grab_t; - const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); - ImRect grab_bb; - if (is_horizontal) - grab_bb = ImRect(ImVec2(grab_pos - grab_sz * 0.5f, frame_bb.Min.y + grab_padding), ImVec2(grab_pos + grab_sz * 0.5f, frame_bb.Max.y - grab_padding)); - else - grab_bb = ImRect(ImVec2(frame_bb.Min.x + grab_padding, grab_pos - grab_sz * 0.5f), ImVec2(frame_bb.Max.x - grab_padding, grab_pos + grab_sz * 0.5f)); - window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); - - return value_changed; -} - -// Use power!=1.0 for logarithmic sliders. -// Adjust display_format to decorate the value with a prefix or a suffix. -// "%.3f" 1.234 -// "%5.2f secs" 01.23 secs -// "Gold: %.0f" Gold: 1 -bool ImGui::SliderFloat(const char *label, float *v, float v_min, float v_max, const char *display_format, float power) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - - // NB- we don't call ItemSize() yet because we may turn into a text edit box below - if (!ItemAdd(total_bb, id, &frame_bb)) - { - ItemSize(total_bb, style.FramePadding.y); - return false; - } - const bool hovered = ItemHoverable(frame_bb, id); - - if (!display_format) - display_format = "%.3f"; - int decimal_precision = ParseFormatPrecision(display_format, 3); - - // Tabbing or CTRL-clicking on Slider turns it into an input box - bool start_text_input = false; - const bool tab_focus_requested = FocusableItemRegister(window, id); - if (tab_focus_requested || (hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || (g.NavInputId == id && g.ScalarAsInputTextId != id)) - { - SetActiveID(id, window); - SetFocusID(id, window); - FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - if (tab_focus_requested || g.IO.KeyCtrl || g.NavInputId == id) - { - start_text_input = true; - g.ScalarAsInputTextId = 0; - } - } - if (start_text_input || (g.ActiveId == id && g.ScalarAsInputTextId == id)) - return InputScalarAsWidgetReplacement(frame_bb, label, ImGuiDataType_Float, v, id, decimal_precision); - - // Actual slider behavior + render grab - ItemSize(total_bb, style.FramePadding.y); - const bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision); - - // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. - char value_buf[64]; - const char *value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); - RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); - - if (label_size.x > 0.0f) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - - return value_changed; -} - -bool ImGui::VSliderFloat(const char *label, const ImVec2 &size, float *v, float v_min, float v_max, const char *display_format, float power) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); - const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - - ItemSize(bb, style.FramePadding.y); - if (!ItemAdd(frame_bb, id)) - return false; - const bool hovered = ItemHoverable(frame_bb, id); + if (!IsWindowContentHoverable(ref_window, flags)) + return false; + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != ref_window->MoveId) + return false; - if (!display_format) - display_format = "%.3f"; - int decimal_precision = ParseFormatPrecision(display_format, 3); + // When changing hovered window we requires a bit of stationary delay before activating hover timer. + // FIXME: We don't support delay other than stationary one for now, other delay would need a way + // to fulfill the possibility that multiple IsWindowHovered() with varying flag could return true + // for different windows of the hierarchy. Possibly need a Hash(Current+Flags) ==> (Timer) cache. + // We can implement this for _Stationary because the data is linked to HoveredWindow rather than CurrentWindow. + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverWindowUnlockedStationaryId != ref_window->ID) + return false; + + return true; +} - if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavInputId == id) - { - SetActiveID(id, window); - SetFocusID(id, window); - FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); - } +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.NavWindow; + ImGuiWindow* cur_window = g.CurrentWindow; - // Actual slider behavior + render grab - bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision, ImGuiSliderFlags_Vertical); + if (ref_window == NULL) + return false; + if (flags & ImGuiFocusedFlags_AnyWindow) + return true; - // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. - // For the vertical slider we allow centered text to overlap the frame padding - char value_buf[64]; - char *value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); - RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); - if (label_size.x > 0.0f) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); - return value_changed; + if (flags & ImGuiHoveredFlags_ChildWindows) + return IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + return (ref_window == cur_window); } -bool ImGui::SliderAngle(const char *label, float *v_rad, float v_degrees_min, float v_degrees_max) +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +// Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmatically. +// If you want a window to never be focused, you may use the e.g. NoInputs flag. +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) { - float v_deg = (*v_rad) * 360.0f / (2 * IM_PI); - bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, "%.0f deg", 1.0f); - *v_rad = v_deg * (2 * IM_PI) / 360.0f; - return value_changed; + return window->WasActive && window == window->RootWindow && !(window->Flags & ImGuiWindowFlags_NoNavFocus); } -bool ImGui::SliderInt(const char *label, int *v, int v_min, int v_max, const char *display_format) +float ImGui::GetWindowWidth() { - if (!display_format) - display_format = "%.0f"; - float v_f = (float) *v; - bool value_changed = SliderFloat(label, &v_f, (float) v_min, (float) v_max, display_format, 1.0f); - *v = (int) v_f; - return value_changed; + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.x; } -bool ImGui::VSliderInt(const char *label, const ImVec2 &size, int *v, int v_min, int v_max, const char *display_format) +float ImGui::GetWindowHeight() { - if (!display_format) - display_format = "%.0f"; - float v_f = (float) *v; - bool value_changed = VSliderFloat(label, size, &v_f, (float) v_min, (float) v_max, display_format, 1.0f); - *v = (int) v_f; - return value_changed; + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.y; } -// Add multiple sliders on 1 line for compact edition of multiple components -bool ImGui::SliderFloatN(const char *label, float *v, int components, float v_min, float v_max, const char *display_format, float power) +ImVec2 ImGui::GetWindowPos() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + return window->Pos; +} - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= SliderFloat("##v", &v[i], v_min, v_max, display_format, power); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); +void ImGui::SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowPosAllowFlags & cond) == 0) + return; - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); - return value_changed; + // Set + const ImVec2 old_pos = window->Pos; + window->Pos = ImTrunc(pos); + ImVec2 offset = window->Pos - old_pos; + if (offset.x == 0.0f && offset.y == 0.0f) + return; + MarkIniSettingsDirty(window); + window->DC.CursorPos += offset; // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor + window->DC.CursorMaxPos += offset; // And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected. + window->DC.IdealMaxPos += offset; + window->DC.CursorStartPos += offset; } -bool ImGui::SliderFloat2(const char *label, float v[2], float v_min, float v_max, const char *display_format, float power) +void ImGui::SetWindowPos(const ImVec2& pos, ImGuiCond cond) { - return SliderFloatN(label, v, 2, v_min, v_max, display_format, power); + ImGuiWindow* window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); } -bool ImGui::SliderFloat3(const char *label, float v[3], float v_min, float v_max, const char *display_format, float power) +void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) { - return SliderFloatN(label, v, 3, v_min, v_max, display_format, power); + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); } -bool ImGui::SliderFloat4(const char *label, float v[4], float v_min, float v_max, const char *display_format, float power) +ImVec2 ImGui::GetWindowSize() { - return SliderFloatN(label, v, 4, v_min, v_max, display_format, power); + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Size; } -bool ImGui::SliderIntN(const char *label, int *v, int components, int v_min, int v_max, const char *display_format) +void ImGui::SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) + return; - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= SliderInt("##v", &v[i], v_min, v_max, display_format); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); + // Enable auto-fit (not done in BeginChild() path unless appearing or combined with ImGuiChildFlags_AlwaysAutoResize) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesX = (size.x <= 0.0f) ? 2 : 0; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesY = (size.y <= 0.0f) ? 2 : 0; - return value_changed; + // Set + ImVec2 old_size = window->SizeFull; + if (size.x <= 0.0f) + window->AutoFitOnlyGrows = false; + else + window->SizeFull.x = IM_TRUNC(size.x); + if (size.y <= 0.0f) + window->AutoFitOnlyGrows = false; + else + window->SizeFull.y = IM_TRUNC(size.y); + if (old_size.x != window->SizeFull.x || old_size.y != window->SizeFull.y) + MarkIniSettingsDirty(window); } -bool ImGui::SliderInt2(const char *label, int v[2], int v_min, int v_max, const char *display_format) +void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) { - return SliderIntN(label, v, 2, v_min, v_max, display_format); + SetWindowSize(GImGui->CurrentWindow, size, cond); } -bool ImGui::SliderInt3(const char *label, int v[3], int v_min, int v_max, const char *display_format) +void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) { - return SliderIntN(label, v, 3, v_min, v_max, display_format); -} - -bool ImGui::SliderInt4(const char *label, int v[4], int v_min, int v_max, const char *display_format) -{ - return SliderIntN(label, v, 4, v_min, v_max, display_format); + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowSize(window, size, cond); } -bool ImGui::DragBehavior(const ImRect &frame_bb, ImGuiID id, float *v, float v_speed, float v_min, float v_max, int decimal_precision, float power) +void ImGui::SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond) { - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - - // Draw frame - const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : - ImGuiCol_FrameBg); - RenderNavHighlight(frame_bb, id); - RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); - - bool value_changed = false; - - // Process interacting with the drag - if (g.ActiveId == id) - { - if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) - ClearActiveID(); - else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) - ClearActiveID(); - } - if (g.ActiveId == id) - { - if (g.ActiveIdIsJustActivated) - { - // Lock current value on click - g.DragCurrentValue = *v; - g.DragLastMouseDelta = ImVec2(0.f, 0.f); - } - - if (v_speed == 0.0f && (v_max - v_min) != 0.0f && (v_max - v_min) < FLT_MAX) - v_speed = (v_max - v_min) * g.DragSpeedDefaultRatio; - - float v_cur = g.DragCurrentValue; - const ImVec2 mouse_drag_delta = GetMouseDragDelta(0, 1.0f); - float adjust_delta = 0.0f; - if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid()) - { - adjust_delta = mouse_drag_delta.x - g.DragLastMouseDelta.x; - if (g.IO.KeyShift && g.DragSpeedScaleFast >= 0.0f) - adjust_delta *= g.DragSpeedScaleFast; - if (g.IO.KeyAlt && g.DragSpeedScaleSlow >= 0.0f) - adjust_delta *= g.DragSpeedScaleSlow; - g.DragLastMouseDelta.x = mouse_drag_delta.x; - } - if (g.ActiveIdSource == ImGuiInputSource_Nav) - { - adjust_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 1.0f / 10.0f, 10.0f).x; - if (v_min < v_max && ((v_cur >= v_max && adjust_delta > 0.0f) || (v_cur <= v_min && adjust_delta < 0.0f))) // This is to avoid applying the saturation when already past the limits - adjust_delta = 0.0f; - v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); - } - adjust_delta *= v_speed; - - if (fabsf(adjust_delta) > 0.0f) - { - if (fabsf(power - 1.0f) > 0.001f) - { - // Logarithmic curve on both side of 0.0 - float v0_abs = v_cur >= 0.0f ? v_cur : -v_cur; - float v0_sign = v_cur >= 0.0f ? 1.0f : -1.0f; - float v1 = powf(v0_abs, 1.0f / power) + (adjust_delta * v0_sign); - float v1_abs = v1 >= 0.0f ? v1 : -v1; - float v1_sign = v1 >= 0.0f ? 1.0f : -1.0f; // Crossed sign line - v_cur = powf(v1_abs, power) * v0_sign * v1_sign; // Reapply sign - } - else - { - v_cur += adjust_delta; - } - - // Clamp - if (v_min < v_max) - v_cur = ImClamp(v_cur, v_min, v_max); - g.DragCurrentValue = v_cur; - } - - // Round to user desired precision, then apply - v_cur = RoundScalar(v_cur, decimal_precision); - if (*v != v_cur) - { - *v = v_cur; - value_changed = true; - } - } - - return value_changed; -} - -bool ImGui::DragFloat(const char *label, float *v, float v_speed, float v_min, float v_max, const char *display_format, float power) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); - const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - - // NB- we don't call ItemSize() yet because we may turn into a text edit box below - if (!ItemAdd(total_bb, id, &frame_bb)) - { - ItemSize(total_bb, style.FramePadding.y); - return false; - } - const bool hovered = ItemHoverable(frame_bb, id); - - if (!display_format) - display_format = "%.3f"; - int decimal_precision = ParseFormatPrecision(display_format, 3); - - // Tabbing or CTRL-clicking on Drag turns it into an input box - bool start_text_input = false; - const bool tab_focus_requested = FocusableItemRegister(window, id); - if (tab_focus_requested || (hovered && (g.IO.MouseClicked[0] || g.IO.MouseDoubleClicked[0])) || g.NavActivateId == id || (g.NavInputId == id && g.ScalarAsInputTextId != id)) - { - SetActiveID(id, window); - SetFocusID(id, window); - FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - if (tab_focus_requested || g.IO.KeyCtrl || g.IO.MouseDoubleClicked[0] || g.NavInputId == id) - { - start_text_input = true; - g.ScalarAsInputTextId = 0; - } - } - if (start_text_input || (g.ActiveId == id && g.ScalarAsInputTextId == id)) - return InputScalarAsWidgetReplacement(frame_bb, label, ImGuiDataType_Float, v, id, decimal_precision); - - // Actual drag behavior - ItemSize(total_bb, style.FramePadding.y); - const bool value_changed = DragBehavior(frame_bb, id, v, v_speed, v_min, v_max, decimal_precision, power); - - // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. - char value_buf[64]; - const char *value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); - RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); - - if (label_size.x > 0.0f) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); - - return value_changed; -} - -bool ImGui::DragFloatN(const char *label, float *v, int components, float v_speed, float v_min, float v_max, const char *display_format, float power) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= DragFloat("##v", &v[i], v_speed, v_min, v_max, display_format, power); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowCollapsedAllowFlags & cond) == 0) + return; + window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - - return value_changed; + // Set + window->Collapsed = collapsed; } -bool ImGui::DragFloat2(const char *label, float v[2], float v_speed, float v_min, float v_max, const char *display_format, float power) +void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size) { - return DragFloatN(label, v, 2, v_speed, v_min, v_max, display_format, power); + IM_ASSERT(window->HitTestHoleSize.x == 0); // We don't support multiple holes/hit test filters + window->HitTestHoleSize = ImVec2ih(size); + window->HitTestHoleOffset = ImVec2ih(pos - window->Pos); } -bool ImGui::DragFloat3(const char *label, float v[3], float v_speed, float v_min, float v_max, const char *display_format, float power) +void ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window) { - return DragFloatN(label, v, 3, v_speed, v_min, v_max, display_format, power); + window->Hidden = window->SkipItems = true; + window->HiddenFramesCanSkipItems = 1; } -bool ImGui::DragFloat4(const char *label, float v[4], float v_speed, float v_min, float v_max, const char *display_format, float power) +void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) { - return DragFloatN(label, v, 4, v_speed, v_min, v_max, display_format, power); + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); } -bool ImGui::DragFloatRange2(const char *label, float *v_current_min, float *v_current_max, float v_speed, float v_min, float v_max, const char *display_format, const char *display_format_max, float power) +bool ImGui::IsWindowCollapsed() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - PushID(label); - BeginGroup(); - PushMultiItemsWidths(2); - - bool value_changed = DragFloat("##min", v_current_min, v_speed, (v_min >= v_max) ? -FLT_MAX : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), display_format, power); - PopItemWidth(); - SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= DragFloat("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? FLT_MAX : v_max, display_format_max ? display_format_max : display_format, power); - PopItemWidth(); - SameLine(0, g.Style.ItemInnerSpacing.x); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - PopID(); - - return value_changed; + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Collapsed; } -// NB: v_speed is float to allow adjusting the drag speed with more precision -bool ImGui::DragInt(const char *label, int *v, float v_speed, int v_min, int v_max, const char *display_format) +bool ImGui::IsWindowAppearing() { - if (!display_format) - display_format = "%.0f"; - float v_f = (float) *v; - bool value_changed = DragFloat(label, &v_f, v_speed, (float) v_min, (float) v_max, display_format); - *v = (int) v_f; - return value_changed; + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Appearing; } -bool ImGui::DragIntN(const char *label, int *v, int components, float v_speed, int v_min, int v_max, const char *display_format) +void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); +} - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= DragInt("##v", &v[i], v_speed, v_min, v_max, display_format); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} - return value_changed; +void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasPos; + g.NextWindowData.PosVal = pos; + g.NextWindowData.PosPivotVal = pivot; + g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; } -bool ImGui::DragInt2(const char *label, int v[2], float v_speed, int v_min, int v_max, const char *display_format) +void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) { - return DragIntN(label, v, 2, v_speed, v_min, v_max, display_format); + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSize; + g.NextWindowData.SizeVal = size; + g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; } -bool ImGui::DragInt3(const char *label, int v[3], float v_speed, int v_min, int v_max, const char *display_format) +// For each axis: +// - Use 0.0f as min or FLT_MAX as max if you don't want limits, e.g. size_min = (500.0f, 0.0f), size_max = (FLT_MAX, FLT_MAX) sets a minimum width. +// - Use -1 for both min and max of same axis to preserve current size which itself is a constraint. +// - See "Demo->Examples->Constrained-resizing window" for examples. +void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) { - return DragIntN(label, v, 3, v_speed, v_min, v_max, display_format); + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSizeConstraint; + g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); + g.NextWindowData.SizeCallback = custom_callback; + g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; } -bool ImGui::DragInt4(const char *label, int v[4], float v_speed, int v_min, int v_max, const char *display_format) +// Content size = inner scrollable rectangle, padded with WindowPadding. +// SetNextWindowContentSize(ImVec2(100,100) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item. +void ImGui::SetNextWindowContentSize(const ImVec2& size) { - return DragIntN(label, v, 4, v_speed, v_min, v_max, display_format); + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasContentSize; + g.NextWindowData.ContentSizeVal = ImTrunc(size); } -bool ImGui::DragIntRange2(const char *label, int *v_current_min, int *v_current_max, float v_speed, int v_min, int v_max, const char *display_format, const char *display_format_max) +void ImGui::SetNextWindowScroll(const ImVec2& scroll) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasScroll; + g.NextWindowData.ScrollVal = scroll; +} - ImGuiContext &g = *GImGui; - PushID(label); - BeginGroup(); - PushMultiItemsWidths(2); +void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasCollapsed; + g.NextWindowData.CollapsedVal = collapsed; + g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; +} - bool value_changed = DragInt("##min", v_current_min, v_speed, (v_min >= v_max) ? INT_MIN : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), display_format); - PopItemWidth(); - SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= DragInt("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? INT_MAX : v_max, display_format_max ? display_format_max : display_format); - PopItemWidth(); - SameLine(0, g.Style.ItemInnerSpacing.x); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - PopID(); - - return value_changed; -} - -void ImGui::PlotEx(ImGuiPlotType plot_type, const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - if (graph_size.x == 0.0f) - graph_size.x = CalcItemWidth(); - if (graph_size.y == 0.0f) - graph_size.y = label_size.y + (style.FramePadding.y * 2); +void ImGui::SetNextWindowFocus() +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasFocus; +} - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(graph_size.x, graph_size.y)); - const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); - ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, 0, &frame_bb)) - return; - const bool hovered = ItemHoverable(inner_bb, 0); - - // Determine scale from values if not specified - if (scale_min == FLT_MAX || scale_max == FLT_MAX) - { - float v_min = FLT_MAX; - float v_max = -FLT_MAX; - for (int i = 0; i < values_count; i++) - { - const float v = values_getter(data, i); - v_min = ImMin(v_min, v); - v_max = ImMax(v_max, v); - } - if (scale_min == FLT_MAX) - scale_min = v_min; - if (scale_max == FLT_MAX) - scale_max = v_max; - } - - RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - - if (values_count > 0) - { - int res_w = ImMin((int) graph_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); - int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); - - // Tooltip on hover - int v_hovered = -1; - if (hovered) - { - const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); - const int v_idx = (int) (t * item_count); - IM_ASSERT(v_idx >= 0 && v_idx < values_count); - - const float v0 = values_getter(data, (v_idx + values_offset) % values_count); - const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); - if (plot_type == ImGuiPlotType_Lines) - SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1); - else if (plot_type == ImGuiPlotType_Histogram) - SetTooltip("%d: %8.4g", v_idx, v0); - v_hovered = v_idx; - } - - const float t_step = 1.0f / (float) res_w; - const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); - - float v0 = values_getter(data, (0 + values_offset) % values_count); - float t0 = 0.0f; - ImVec2 tp0 = ImVec2(t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale)); // Point in the normalized space of our target rectangle - float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (-scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands - - const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); - const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); - - for (int n = 0; n < res_w; n++) - { - const float t1 = t0 + t_step; - const int v1_idx = (int) (t0 * item_count + 0.5f); - IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); - const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); - const ImVec2 tp1 = ImVec2(t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale)); - - // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. - ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); - ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); - if (plot_type == ImGuiPlotType_Lines) - { - window->DrawList->AddLine(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); - } - else if (plot_type == ImGuiPlotType_Histogram) - { - if (pos1.x >= pos0.x + 2.0f) - pos1.x -= 1.0f; - window->DrawList->AddRectFilled(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); - } - - t0 = t1; - tp0 = tp1; - } - } - - // Text overlay - if (overlay_text) - RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); - - if (label_size.x > 0.0f) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); -} - -struct ImGuiPlotArrayGetterData -{ - const float *Values; - int Stride; - - ImGuiPlotArrayGetterData(const float *values, int stride) - { - Values = values; - Stride = stride; - } -}; +void ImGui::SetNextWindowBgAlpha(float alpha) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasBgAlpha; + g.NextWindowData.BgAlphaVal = alpha; +} -static float Plot_ArrayGetter(void *data, int idx) +// This is experimental and meant to be a toy for exploring a future/wider range of features. +void ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) { - ImGuiPlotArrayGetterData *plot_data = (ImGuiPlotArrayGetterData *) data; - const float v = *(float *) (void *) ((unsigned char *) plot_data->Values + (size_t) idx * plot_data->Stride); - return v; + ImGuiContext& g = *GImGui; + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasRefreshPolicy; + g.NextWindowData.RefreshFlagsVal = flags; } -void ImGui::PlotLines(const char *label, const float *values, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +ImDrawList* ImGui::GetWindowDrawList() { - ImGuiPlotArrayGetterData data(values, stride); - PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void *) &data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + ImGuiWindow* window = GetCurrentWindow(); + return window->DrawList; } -void ImGui::PlotLines(const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +ImFont* ImGui::GetFont() { - PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + return GImGui->Font; } -void ImGui::PlotHistogram(const char *label, const float *values, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +float ImGui::GetFontSize() { - ImGuiPlotArrayGetterData data(values, stride); - PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void *) &data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + return GImGui->FontSize; } -void ImGui::PlotHistogram(const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +ImVec2 ImGui::GetFontTexUvWhitePixel() { - PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + return GImGui->DrawListSharedData.TexUvWhitePixel; } -// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size -void ImGui::ProgressBar(float fraction, const ImVec2 &size_arg, const char *overlay) +void ImGui::SetWindowFontScale(float scale) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + IM_ASSERT(scale > 0.0f); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->FontWindowScale = scale; + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; +} - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; +void ImGui::PushFocusScope(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiFocusScopeData data; + data.ID = id; + data.WindowID = g.CurrentWindow->ID; + g.FocusScopeStack.push_back(data); + g.CurrentFocusScopeId = id; +} - ImVec2 pos = window->DC.CursorPos; - ImRect bb(pos, pos + CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f)); - ItemSize(bb, style.FramePadding.y); - if (!ItemAdd(bb, 0)) - return; +void ImGui::PopFocusScope() +{ + ImGuiContext& g = *GImGui; + if (g.FocusScopeStack.Size == 0) + { + IM_ASSERT_USER_ERROR(g.FocusScopeStack.Size > 0, "Calling PopFocusScope() too many times!"); + return; + } + g.FocusScopeStack.pop_back(); + g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; +} - // Render - fraction = ImSaturate(fraction); - RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); - const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); - RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); +void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) +{ + ImGuiContext& g = *GImGui; + g.NavFocusScopeId = focus_scope_id; + g.NavFocusRoute.resize(0); // Invalidate + if (focus_scope_id == 0) + return; + IM_ASSERT(g.NavWindow != NULL); - // Default displaying the fraction as percentage string, but user can override it - char overlay_buf[32]; - if (!overlay) - { - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); - overlay = overlay_buf; - } + // Store current path (in reverse order) + if (focus_scope_id == g.CurrentFocusScopeId) + { + // Top of focus stack contains local focus scopes inside current window + for (int n = g.FocusScopeStack.Size - 1; n >= 0 && g.FocusScopeStack.Data[n].WindowID == g.CurrentWindow->ID; n--) + g.NavFocusRoute.push_back(g.FocusScopeStack.Data[n]); + } + else if (focus_scope_id == g.NavWindow->NavRootFocusScopeId) + g.NavFocusRoute.push_back({ focus_scope_id, g.NavWindow->ID }); + else + return; - ImVec2 overlay_size = CalcTextSize(overlay, NULL); - if (overlay_size.x > 0.0f) - RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); + // Then follow on manually set ParentWindowForFocusRoute field (#6798) + for (ImGuiWindow* window = g.NavWindow->ParentWindowForFocusRoute; window != NULL; window = window->ParentWindowForFocusRoute) + g.NavFocusRoute.push_back({ window->NavRootFocusScopeId, window->ID }); + IM_ASSERT(g.NavFocusRoute.Size < 100); // Maximum depth is technically 251 as per CalcRoutingScore(): 254 - 3 } -bool ImGui::Checkbox(const char *label, bool *v) +// Focus = move navigation cursor, set scrolling, set focus window. +void ImGui::FocusItem() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - - const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y * 2, label_size.y + style.FramePadding.y * 2)); // We want a square shape to we use Y twice - ItemSize(check_bb, style.FramePadding.y); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_FOCUS("FocusItem(0x%08x) in window \"%s\"\n", g.LastItemData.ID, window->Name); + if (g.DragDropActive || g.MovingWindow != NULL) // FIXME: Opt-in flags for this? + { + IMGUI_DEBUG_LOG_FOCUS("FocusItem() ignored while DragDropActive!\n"); + return; + } - ImRect total_bb = check_bb; - if (label_size.x > 0) - SameLine(0, style.ItemInnerSpacing.x); - const ImRect text_bb(window->DC.CursorPos + ImVec2(0, style.FramePadding.y), window->DC.CursorPos + ImVec2(0, style.FramePadding.y) + label_size); - if (label_size.x > 0) - { - ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); - total_bb = ImRect(ImMin(check_bb.Min, text_bb.Min), ImMax(check_bb.Max, text_bb.Max)); - } + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavHighlight | ImGuiNavMoveFlags_NoSelect; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + SetNavWindow(window); + NavMoveRequestSubmit(ImGuiDir_None, ImGuiDir_Up, move_flags, scroll_flags); + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); +} - if (!ItemAdd(total_bb, id)) - return false; +void ImGui::ActivateItemByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; + g.NavNextActivateFlags = ImGuiActivateFlags_None; +} - bool hovered, held; - bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) - *v = !(*v); +// Note: this will likely be called ActivateItem() once we rework our Focus/Activation system! +// But ActivateItem() should function without altering scroll/focus? +void ImGui::SetKeyboardFocusHere(int offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(offset >= -1); // -1 is allowed but not below + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere(%d) in window \"%s\"\n", offset, window->Name); - RenderNavHighlight(total_bb, id); - RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : - ImGuiCol_FrameBg), - true, style.FrameRounding); - if (*v) - { - const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); - const float pad = ImMax(1.0f, (float) (int) (check_sz / 6.0f)); - RenderCheckMark(check_bb.Min + ImVec2(pad, pad), GetColorU32(ImGuiCol_CheckMark), check_bb.GetWidth() - pad * 2.0f); - } + // It makes sense in the vast majority of cases to never interrupt a drag and drop. + // When we refactor this function into ActivateItem() we may want to make this an option. + // MovingWindow is protected from most user inputs using SetActiveIdUsingNavAndKeys(), but + // is also automatically dropped in the event g.ActiveId is stolen. + if (g.DragDropActive || g.MovingWindow != NULL) + { + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere() ignored while DragDropActive!\n"); + return; + } - if (g.LogEnabled) - LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]"); - if (label_size.x > 0.0f) - RenderText(text_bb.Min, label); + SetNavWindow(window); - return pressed; + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavHighlight; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + if (offset == -1) + { + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); + } + else + { + g.NavTabbingDir = 1; + g.NavTabbingCounter = offset + 1; + } } -bool ImGui::CheckboxFlags(const char *label, unsigned int *flags, unsigned int flags_value) +void ImGui::SetItemDefaultFocus() { - bool v = ((*flags & flags_value) == flags_value); - bool pressed = Checkbox(label, &v); - if (pressed) - { - if (v) - *flags |= flags_value; - else - *flags &= ~flags_value; - } - - return pressed; -} - -bool ImGui::RadioButton(const char *label, bool active) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - - const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y * 2 - 1, label_size.y + style.FramePadding.y * 2 - 1)); - ItemSize(check_bb, style.FramePadding.y); - - ImRect total_bb = check_bb; - if (label_size.x > 0) - SameLine(0, style.ItemInnerSpacing.x); - const ImRect text_bb(window->DC.CursorPos + ImVec2(0, style.FramePadding.y), window->DC.CursorPos + ImVec2(0, style.FramePadding.y) + label_size); - if (label_size.x > 0) - { - ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); - total_bb.Add(text_bb); - } - - if (!ItemAdd(total_bb, id)) - return false; - - ImVec2 center = check_bb.GetCenter(); - center.x = (float) (int) center.x + 0.5f; - center.y = (float) (int) center.y + 0.5f; - const float radius = check_bb.GetHeight() * 0.5f; - - bool hovered, held; - bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - - RenderNavHighlight(total_bb, id); - window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : - ImGuiCol_FrameBg), - 16); - if (active) - { - const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); - const float pad = ImMax(1.0f, (float) (int) (check_sz / 6.0f)); - window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark), 16); - } - - if (style.FrameBorderSize > 0.0f) - { - window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize); - window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize); - } + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!window->Appearing) + return; + if (g.NavWindow != window->RootWindowForNav || (!g.NavInitRequest && g.NavInitResult.ID == 0) || g.NavLayer != window->DC.NavLayerCurrent) + return; - if (g.LogEnabled) - LogRenderedText(&text_bb.Min, active ? "(x)" : "( )"); - if (label_size.x > 0.0f) - RenderText(text_bb.Min, label); + g.NavInitRequest = false; + NavApplyItemToResult(&g.NavInitResult); + NavUpdateAnyRequestFlag(); - return pressed; + // Scroll could be done in NavInitRequestApplyResult() via an opt-in flag (we however don't want regular init requests to scroll) + if (!window->ClipRect.Contains(g.LastItemData.Rect)) + ScrollToRectEx(window, g.LastItemData.Rect, ImGuiScrollFlags_None); } -bool ImGui::RadioButton(const char *label, int *v, int v_button) +void ImGui::SetStateStorage(ImGuiStorage* tree) { - const bool pressed = RadioButton(label, *v == v_button); - if (pressed) - { - *v = v_button; - } - return pressed; + ImGuiWindow* window = GImGui->CurrentWindow; + window->DC.StateStorage = tree ? tree : &window->StateStorage; } -static int InputTextCalcTextLenAndLineCount(const char *text_begin, const char **out_text_end) +ImGuiStorage* ImGui::GetStateStorage() { - int line_count = 0; - const char *s = text_begin; - while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding - if (c == '\n') - line_count++; - s--; - if (s[0] != '\n' && s[0] != '\r') - line_count++; - *out_text_end = s; - return line_count; + ImGuiWindow* window = GImGui->CurrentWindow; + return window->DC.StateStorage; } -static ImVec2 InputTextCalcTextSizeW(const ImWchar *text_begin, const ImWchar *text_end, const ImWchar **remaining, ImVec2 *out_offset, bool stop_on_new_line) +bool ImGui::IsRectVisible(const ImVec2& size) { - ImFont *font = GImGui->Font; - const float line_height = GImGui->FontSize; - const float scale = line_height / font->FontSize; - - ImVec2 text_size = ImVec2(0, 0); - float line_width = 0.0f; - - const ImWchar *s = text_begin; - while (s < text_end) - { - unsigned int c = (unsigned int) (*s++); - if (c == '\n') - { - text_size.x = ImMax(text_size.x, line_width); - text_size.y += line_height; - line_width = 0.0f; - if (stop_on_new_line) - break; - continue; - } - if (c == '\r') - continue; - - const float char_width = font->GetCharAdvance((unsigned short) c) * scale; - line_width += char_width; - } - - if (text_size.x < line_width) - text_size.x = line_width; - - if (out_offset) - *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} - if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n - text_size.y += line_height; +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} - if (remaining) - *remaining = s; +//----------------------------------------------------------------------------- +// [SECTION] ID STACK +//----------------------------------------------------------------------------- - return text_size; +// This is one of the very rare legacy case where we use ImGuiWindow methods, +// it should ideally be flattened at some point but it's been used a lots by widgets. +IM_MSVC_RUNTIME_CHECKS_OFF +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; } -// Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) -namespace ImGuiStb +ImGuiID ImGuiWindow::GetID(const void* ptr) { + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); +#endif + return id; +} -static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING *obj) +ImGuiID ImGuiWindow::GetID(int n) { - return obj->CurLenW; + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; } -static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING *obj, int idx) + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +// FIXME: Consider instead storing last non-zero ID + count of successive zero-ID, and combine those? +ImGuiID ImGuiWindow::GetIDFromPos(const ImVec2& p_abs) { - return obj->Text[idx]; + ImGuiID seed = IDStack.back(); + ImVec2 p_rel = ImGui::WindowPosAbsToRel(this, p_abs); + ImGuiID id = ImHashData(&p_rel, sizeof(p_rel), seed); + return id; } -static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING *obj, int line_start_idx, int char_idx) + +// " +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) { - ImWchar c = obj->Text[line_start_idx + char_idx]; - if (c == '\n') - return STB_TEXTEDIT_GETWIDTH_NEWLINE; - return GImGui->Font->GetCharAdvance(c) * (GImGui->FontSize / GImGui->Font->FontSize); + ImGuiID seed = IDStack.back(); + ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); + ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); + return id; } -static int STB_TEXTEDIT_KEYTOTEXT(int key) + +void ImGui::PushID(const char* str_id) { - return key >= 0x10000 ? 0 : key; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id); + window->IDStack.push_back(id); } -static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; -static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow *r, STB_TEXTEDIT_STRING *obj, int line_start_idx) + +void ImGui::PushID(const char* str_id_begin, const char* str_id_end) { - const ImWchar *text = obj->Text.Data; - const ImWchar *text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); - r->x0 = 0.0f; - r->x1 = size.x; - r->baseline_y_delta = size.y; - r->ymin = 0.0f; - r->ymax = size.y; - r->num_chars = (int) (text_remaining - (text + line_start_idx)); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id_begin, str_id_end); + window->IDStack.push_back(id); } -static bool is_separator(unsigned int c) +void ImGui::PushID(const void* ptr_id) { - return ImCharIsSpace(c) || c == ',' || c == ';' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c == ']' || c == '|'; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(ptr_id); + window->IDStack.push_back(id); } -static int is_word_boundary_from_right(STB_TEXTEDIT_STRING *obj, int idx) + +void ImGui::PushID(int int_id) { - return idx > 0 ? (is_separator(obj->Text[idx - 1]) && !is_separator(obj->Text[idx])) : 1; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(int_id); + window->IDStack.push_back(id); } -static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) + +// Push a given id value ignoring the ID stack as a seed. +void ImGui::PushOverrideID(ImGuiID id) { - idx--; - while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) - idx--; - return idx < 0 ? 0 : idx; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL); +#endif + window->IDStack.push_back(id); } -#ifdef __APPLE__ // FIXME: Move setting to IO structure -static int is_word_boundary_from_left(STB_TEXTEDIT_STRING *obj, int idx) + +// Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call +// (note that when using this pattern, ID Stack Tool will tend to not display the intermediate stack level. +// for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more) +ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) { - return idx > 0 ? (!is_separator(obj->Text[idx - 1]) && is_separator(obj->Text[idx])) : 1; + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) + +ImGuiID ImGui::GetIDWithSeed(int n, ImGuiID seed) { - idx++; - int len = obj->CurLenW; - while (idx < len && !is_word_boundary_from_left(obj, idx)) - idx++; - return idx > len ? len : idx; + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; } -#else -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) + +void ImGui::PopID() { - idx++; - int len = obj->CurLenW; - while (idx < len && !is_word_boundary_from_right(obj, idx)) - idx++; - return idx > len ? len : idx; + ImGuiWindow* window = GImGui->CurrentWindow; + IM_ASSERT(window->IDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + window->IDStack.pop_back(); } -#endif -#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h -#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL -static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING *obj, int pos, int n) +ImGuiID ImGui::GetID(const char* str_id) { - ImWchar *dst = obj->Text.Data + pos; - - // We maintain our buffer length in both UTF-8 and wchar formats - obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); - obj->CurLenW -= n; - - // Offset remaining text - const ImWchar *src = obj->Text.Data + pos + n; - while (ImWchar c = *src++) - *dst++ = c; - *dst = '\0'; + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id); } -static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING *obj, int pos, const ImWchar *new_text, int new_text_len) +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) { - const int text_len = obj->CurLenW; - IM_ASSERT(pos <= text_len); - if (new_text_len + text_len + 1 > obj->Text.Size) - return false; - - const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); - if (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufSizeA) - return false; - - ImWchar *text = obj->Text.Data; - if (pos != text_len) - memmove(text + pos + new_text_len, text + pos, (size_t) (text_len - pos) * sizeof(ImWchar)); - memcpy(text + pos, new_text, (size_t) new_text_len * sizeof(ImWchar)); - - obj->CurLenW += new_text_len; - obj->CurLenA += new_text_len_utf8; - obj->Text[obj->CurLenW] = '\0'; + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id_begin, str_id_end); +} - return true; +ImGuiID ImGui::GetID(const void* ptr_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(ptr_id); } -// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols) -#define STB_TEXTEDIT_K_LEFT 0x10000 // keyboard input to move cursor left -#define STB_TEXTEDIT_K_RIGHT 0x10001 // keyboard input to move cursor right -#define STB_TEXTEDIT_K_UP 0x10002 // keyboard input to move cursor up -#define STB_TEXTEDIT_K_DOWN 0x10003 // keyboard input to move cursor down -#define STB_TEXTEDIT_K_LINESTART 0x10004 // keyboard input to move cursor to start of line -#define STB_TEXTEDIT_K_LINEEND 0x10005 // keyboard input to move cursor to end of line -#define STB_TEXTEDIT_K_TEXTSTART 0x10006 // keyboard input to move cursor to start of text -#define STB_TEXTEDIT_K_TEXTEND 0x10007 // keyboard input to move cursor to end of text -#define STB_TEXTEDIT_K_DELETE 0x10008 // keyboard input to delete selection or character under cursor -#define STB_TEXTEDIT_K_BACKSPACE 0x10009 // keyboard input to delete selection or character left of cursor -#define STB_TEXTEDIT_K_UNDO 0x1000A // keyboard input to perform undo -#define STB_TEXTEDIT_K_REDO 0x1000B // keyboard input to perform redo -#define STB_TEXTEDIT_K_WORDLEFT 0x1000C // keyboard input to move cursor left one word -#define STB_TEXTEDIT_K_WORDRIGHT 0x1000D // keyboard input to move cursor right one word -#define STB_TEXTEDIT_K_SHIFT 0x20000 - -#define STB_TEXTEDIT_IMPLEMENTATION -#include "stb_textedit.h" - -} // namespace ImGuiStb - -void ImGuiTextEditState::OnKeyPressed(int key) -{ - stb_textedit_key(this, &StbState, key); - CursorFollow = true; - CursorAnimReset(); -} - -// Public API to manipulate UTF-8 text -// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) -// FIXME: The existence of this rarely exercised code path is a bit of a nuisance. -void ImGuiTextEditCallbackData::DeleteChars(int pos, int bytes_count) -{ - IM_ASSERT(pos + bytes_count <= BufTextLen); - char *dst = Buf + pos; - const char *src = Buf + pos + bytes_count; - while (char c = *src++) - *dst++ = c; - *dst = '\0'; - - if (CursorPos + bytes_count >= pos) - CursorPos -= bytes_count; - else if (CursorPos >= pos) - CursorPos = pos; - SelectionStart = SelectionEnd = CursorPos; - BufDirty = true; - BufTextLen -= bytes_count; -} - -void ImGuiTextEditCallbackData::InsertChars(int pos, const char *new_text, const char *new_text_end) -{ - const int new_text_len = new_text_end ? (int) (new_text_end - new_text) : (int) strlen(new_text); - if (new_text_len + BufTextLen + 1 >= BufSize) - return; - - if (BufTextLen != pos) - memmove(Buf + pos + new_text_len, Buf + pos, (size_t) (BufTextLen - pos)); - memcpy(Buf + pos, new_text, (size_t) new_text_len * sizeof(char)); - Buf[BufTextLen + new_text_len] = '\0'; - - if (CursorPos >= pos) - CursorPos += new_text_len; - SelectionStart = SelectionEnd = CursorPos; - BufDirty = true; - BufTextLen += new_text_len; -} - -// Return false to discard a character. -static bool InputTextFilterCharacter(unsigned int *p_char, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void *user_data) -{ - unsigned int c = *p_char; - - if (c < 128 && c != ' ' && !isprint((int) (c & 0xFF))) - { - bool pass = false; - pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); - pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); - if (!pass) - return false; - } - - if (c >= 0xE000 && c <= 0xF8FF) // Filter private Unicode range. I don't imagine anybody would want to input them. GLFW on OSX seems to send private characters for special keys like arrow keys. - return false; - - if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank)) - { - if (flags & ImGuiInputTextFlags_CharsDecimal) - if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/')) - return false; - - if (flags & ImGuiInputTextFlags_CharsHexadecimal) - if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) - return false; - - if (flags & ImGuiInputTextFlags_CharsUppercase) - if (c >= 'a' && c <= 'z') - *p_char = (c += (unsigned int) ('A' - 'a')); - - if (flags & ImGuiInputTextFlags_CharsNoBlank) - if (ImCharIsSpace(c)) - return false; - } - - if (flags & ImGuiInputTextFlags_CallbackCharFilter) - { - ImGuiTextEditCallbackData callback_data; - memset(&callback_data, 0, sizeof(ImGuiTextEditCallbackData)); - callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; - callback_data.EventChar = (ImWchar) c; - callback_data.Flags = flags; - callback_data.UserData = user_data; - if (callback(&callback_data) != 0) - return false; - *p_char = callback_data.EventChar; - if (!callback_data.EventChar) - return false; - } - - return true; -} - -// Edit a string of text -// NB: when active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while active has no effect. -// FIXME: Rather messy function partly because we are doing UTF8 > u16 > UTF8 conversions on the go to more easily handle stb_textedit calls. Ideally we should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188 -bool ImGui::InputTextEx(const char *label, char *buf, int buf_size, const ImVec2 &size_arg, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void *user_data) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) - IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) - - ImGuiContext &g = *GImGui; - const ImGuiIO &io = g.IO; - const ImGuiStyle &style = g.Style; - - const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; - const bool is_editable = (flags & ImGuiInputTextFlags_ReadOnly) == 0; - const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; - const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; - - if (is_multiline) // Open group before calling GetID() because groups tracks id created during their spawn - BeginGroup(); - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? GetTextLineHeight() * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? (style.ItemInnerSpacing.x + label_size.x) : 0.0f, 0.0f)); - - ImGuiWindow *draw_window = window; - if (is_multiline) - { - ItemAdd(total_bb, id, &frame_bb); - if (!BeginChildFrame(id, frame_bb.GetSize())) - { - EndChildFrame(); - EndGroup(); - return false; - } - draw_window = GetCurrentWindow(); - size.x -= draw_window->ScrollbarSizes.x; - } - else - { - ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) - return false; - } - const bool hovered = ItemHoverable(frame_bb, id); - if (hovered) - g.MouseCursor = ImGuiMouseCursor_TextInput; - - // Password pushes a temporary font with only a fallback glyph - if (is_password) - { - const ImFontGlyph *glyph = g.Font->FindGlyph('*'); - ImFont *password_font = &g.InputTextPasswordFont; - password_font->FontSize = g.Font->FontSize; - password_font->Scale = g.Font->Scale; - password_font->DisplayOffset = g.Font->DisplayOffset; - password_font->Ascent = g.Font->Ascent; - password_font->Descent = g.Font->Descent; - password_font->ContainerAtlas = g.Font->ContainerAtlas; - password_font->FallbackGlyph = glyph; - password_font->FallbackAdvanceX = glyph->AdvanceX; - IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); - PushFont(password_font); - } - - // NB: we are only allowed to access 'edit_state' if we are the active widget. - ImGuiTextEditState &edit_state = g.InputTextState; - - const bool focus_requested = FocusableItemRegister(window, id, (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) == 0); // Using completion callback disable keyboard tabbing - const bool focus_requested_by_code = focus_requested && (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent); - const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code; - - const bool user_clicked = hovered && io.MouseClicked[0]; - const bool user_scrolled = is_multiline && g.ActiveId == 0 && edit_state.Id == id && g.ActiveIdPreviousFrame == draw_window->GetIDNoKeepAlive("#SCROLLY"); - - bool clear_active_id = false; - - bool select_all = (g.ActiveId != id) && (((flags & ImGuiInputTextFlags_AutoSelectAll) != 0) || (g.NavInputId == id)) && (!is_multiline); - if (focus_requested || user_clicked || user_scrolled || g.NavInputId == id) - { - if (g.ActiveId != id) - { - // Start edition - // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) - // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) - const int prev_len_w = edit_state.CurLenW; - edit_state.Text.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data isn't NULL so it doesn't crash. - edit_state.InitialText.resize(buf_size + 1); // UTF-8. we use +1 to make sure that .Data isn't NULL so it doesn't crash. - ImStrncpy(edit_state.InitialText.Data, buf, edit_state.InitialText.Size); - const char *buf_end = NULL; - edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, buf, NULL, &buf_end); - edit_state.CurLenA = (int) (buf_end - buf); // We can't get the result from ImFormatString() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. - edit_state.CursorAnimReset(); - - // Preserve cursor position and undo/redo stack if we come back to same widget - // FIXME: We should probably compare the whole buffer to be on the safety side. Comparing buf (utf8) and edit_state.Text (wchar). - const bool recycle_state = (edit_state.Id == id) && (prev_len_w == edit_state.CurLenW); - if (recycle_state) - { - // Recycle existing cursor/selection/undo stack but clamp position - // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. - edit_state.CursorClamp(); - } - else - { - edit_state.Id = id; - edit_state.ScrollX = 0.0f; - stb_textedit_initialize_state(&edit_state.StbState, !is_multiline); - if (!is_multiline && focus_requested_by_code) - select_all = true; - } - if (flags & ImGuiInputTextFlags_AlwaysInsertMode) - edit_state.StbState.insert_mode = true; - if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl))) - select_all = true; - } - SetActiveID(id, window); - SetFocusID(id, window); - FocusWindow(window); - if (!is_multiline && !(flags & ImGuiInputTextFlags_CallbackHistory)) - g.ActiveIdAllowNavDirFlags |= ((1 << ImGuiDir_Up) | (1 << ImGuiDir_Down)); - } - else if (io.MouseClicked[0]) - { - // Release focus when we click outside - clear_active_id = true; - } - - bool value_changed = false; - bool enter_pressed = false; - - if (g.ActiveId == id) - { - if (!is_editable && !g.ActiveIdIsJustActivated) - { - // When read-only we always use the live data passed to the function - edit_state.Text.resize(buf_size + 1); - const char *buf_end = NULL; - edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, buf, NULL, &buf_end); - edit_state.CurLenA = (int) (buf_end - buf); - edit_state.CursorClamp(); - } - - edit_state.BufSizeA = buf_size; - - // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. - // Down the line we should have a cleaner library-wide concept of Selected vs Active. - g.ActiveIdAllowOverlap = !io.MouseDown[0]; - g.WantTextInputNextFrame = 1; - - // Edit in progress - const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + edit_state.ScrollX; - const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize * 0.5f)); - - const bool osx_double_click_selects_words = io.OptMacOSXBehaviors; // OS X style: Double click selects by word instead of selecting whole text - if (select_all || (hovered && !osx_double_click_selects_words && io.MouseDoubleClicked[0])) - { - edit_state.SelectAll(); - edit_state.SelectedAllMouseLock = true; - } - else if (hovered && osx_double_click_selects_words && io.MouseDoubleClicked[0]) - { - // Select a word only, OS X style (by simulating keystrokes) - edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); - edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); - } - else if (io.MouseClicked[0] && !edit_state.SelectedAllMouseLock) - { - if (hovered) - { - stb_textedit_click(&edit_state, &edit_state.StbState, mouse_x, mouse_y); - edit_state.CursorAnimReset(); - } - } - else if (io.MouseDown[0] && !edit_state.SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) - { - stb_textedit_drag(&edit_state, &edit_state.StbState, mouse_x, mouse_y); - edit_state.CursorAnimReset(); - edit_state.CursorFollow = true; - } - if (edit_state.SelectedAllMouseLock && !io.MouseDown[0]) - edit_state.SelectedAllMouseLock = false; - - if (io.InputCharacters[0]) - { - // Process text input (before we check for Return because using some IME will effectively send a Return?) - // We ignore CTRL inputs, but need to allow CTRL+ALT as some keyboards (e.g. German) use AltGR - which is Alt+Ctrl - to input certain characters. - if (!(io.KeyCtrl && !io.KeyAlt) && is_editable) - { - for (int n = 0; n < IM_ARRAYSIZE(io.InputCharacters) && io.InputCharacters[n]; n++) - if (unsigned int c = (unsigned int) io.InputCharacters[n]) - { - // Insert character if they pass filtering - if (!InputTextFilterCharacter(&c, flags, callback, user_data)) - continue; - edit_state.OnKeyPressed((int) c); - } - } - - // Consume characters - memset(g.IO.InputCharacters, 0, sizeof(g.IO.InputCharacters)); - } - } - - bool cancel_edit = false; - if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) - { - // Handle key-presses - const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); - const bool is_shortcut_key_only = (io.OptMacOSXBehaviors ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl - const bool is_wordmove_key_down = io.OptMacOSXBehaviors ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl - const bool is_startend_key_down = io.OptMacOSXBehaviors && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End - const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper; - const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper; - - const bool is_cut = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && is_editable && !is_password && (!is_multiline || edit_state.HasSelection()); - const bool is_copy = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || edit_state.HasSelection()); - const bool is_paste = ((is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && is_editable; - - if (IsKeyPressedMap(ImGuiKey_LeftArrow)) - { - edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : - STB_TEXTEDIT_K_LEFT) | - k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_RightArrow)) - { - edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : - STB_TEXTEDIT_K_RIGHT) | - k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) - { - if (io.KeyCtrl) - SetWindowScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); - else - edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) - { - if (io.KeyCtrl) - SetWindowScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); - else - edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_Home)) - { - edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_End)) - { - edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_Delete) && is_editable) - { - edit_state.OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_Backspace) && is_editable) - { - if (!edit_state.HasSelection()) - { - if (is_wordmove_key_down) - edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT); - else if (io.OptMacOSXBehaviors && io.KeySuper && !io.KeyAlt && !io.KeyCtrl) - edit_state.OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT); - } - edit_state.OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); - } - else if (IsKeyPressedMap(ImGuiKey_Enter)) - { - bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; - if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) - { - enter_pressed = clear_active_id = true; - } - else if (is_editable) - { - unsigned int c = '\n'; // Insert new line - if (InputTextFilterCharacter(&c, flags, callback, user_data)) - edit_state.OnKeyPressed((int) c); - } - } - else if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !io.KeyCtrl && !io.KeyShift && !io.KeyAlt && is_editable) - { - unsigned int c = '\t'; // Insert TAB - if (InputTextFilterCharacter(&c, flags, callback, user_data)) - edit_state.OnKeyPressed((int) c); - } - else if (IsKeyPressedMap(ImGuiKey_Escape)) - { - clear_active_id = cancel_edit = true; - } - else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_Z) && is_editable && is_undoable) - { - edit_state.OnKeyPressed(STB_TEXTEDIT_K_UNDO); - edit_state.ClearSelection(); - } - else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_Y) && is_editable && is_undoable) - { - edit_state.OnKeyPressed(STB_TEXTEDIT_K_REDO); - edit_state.ClearSelection(); - } - else if (is_shortcut_key_only && IsKeyPressedMap(ImGuiKey_A)) - { - edit_state.SelectAll(); - edit_state.CursorFollow = true; - } - else if (is_cut || is_copy) - { - // Cut, Copy - if (io.SetClipboardTextFn) - { - const int ib = edit_state.HasSelection() ? ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end) : 0; - const int ie = edit_state.HasSelection() ? ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end) : edit_state.CurLenW; - edit_state.TempTextBuffer.resize((ie - ib) * 4 + 1); - ImTextStrToUtf8(edit_state.TempTextBuffer.Data, edit_state.TempTextBuffer.Size, edit_state.Text.Data + ib, edit_state.Text.Data + ie); - SetClipboardText(edit_state.TempTextBuffer.Data); - } - - if (is_cut) - { - if (!edit_state.HasSelection()) - edit_state.SelectAll(); - edit_state.CursorFollow = true; - stb_textedit_cut(&edit_state, &edit_state.StbState); - } - } - else if (is_paste) - { - // Paste - if (const char *clipboard = GetClipboardText()) - { - // Filter pasted buffer - const int clipboard_len = (int) strlen(clipboard); - ImWchar *clipboard_filtered = (ImWchar *) ImGui::MemAlloc((clipboard_len + 1) * sizeof(ImWchar)); - int clipboard_filtered_len = 0; - for (const char *s = clipboard; *s;) - { - unsigned int c; - s += ImTextCharFromUtf8(&c, s, NULL); - if (c == 0) - break; - if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, user_data)) - continue; - clipboard_filtered[clipboard_filtered_len++] = (ImWchar) c; - } - clipboard_filtered[clipboard_filtered_len] = 0; - if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation - { - stb_textedit_paste(&edit_state, &edit_state.StbState, clipboard_filtered, clipboard_filtered_len); - edit_state.CursorFollow = true; - } - ImGui::MemFree(clipboard_filtered); - } - } - } - - if (g.ActiveId == id) - { - if (cancel_edit) - { - // Restore initial value - if (is_editable) - { - ImStrncpy(buf, edit_state.InitialText.Data, buf_size); - value_changed = true; - } - } - - // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. - // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage. - bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); - if (apply_edit_back_to_user_buffer) - { - // Apply new value immediately - copy modified buffer back - // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer - // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. - // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. - if (is_editable) - { - edit_state.TempTextBuffer.resize(edit_state.Text.Size * 4); - ImTextStrToUtf8(edit_state.TempTextBuffer.Data, edit_state.TempTextBuffer.Size, edit_state.Text.Data, NULL); - } - - // User callback - if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways)) != 0) - { - IM_ASSERT(callback != NULL); - - // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. - ImGuiInputTextFlags event_flag = 0; - ImGuiKey event_key = ImGuiKey_COUNT; - if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab)) - { - event_flag = ImGuiInputTextFlags_CallbackCompletion; - event_key = ImGuiKey_Tab; - } - else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow)) - { - event_flag = ImGuiInputTextFlags_CallbackHistory; - event_key = ImGuiKey_UpArrow; - } - else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow)) - { - event_flag = ImGuiInputTextFlags_CallbackHistory; - event_key = ImGuiKey_DownArrow; - } - else if (flags & ImGuiInputTextFlags_CallbackAlways) - event_flag = ImGuiInputTextFlags_CallbackAlways; - - if (event_flag) - { - ImGuiTextEditCallbackData callback_data; - memset(&callback_data, 0, sizeof(ImGuiTextEditCallbackData)); - callback_data.EventFlag = event_flag; - callback_data.Flags = flags; - callback_data.UserData = user_data; - callback_data.ReadOnly = !is_editable; - - callback_data.EventKey = event_key; - callback_data.Buf = edit_state.TempTextBuffer.Data; - callback_data.BufTextLen = edit_state.CurLenA; - callback_data.BufSize = edit_state.BufSizeA; - callback_data.BufDirty = false; - - // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) - ImWchar *text = edit_state.Text.Data; - const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.cursor); - const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_start); - const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_end); - - // Call user code - callback(&callback_data); - - // Read back what user may have modified - IM_ASSERT(callback_data.Buf == edit_state.TempTextBuffer.Data); // Invalid to modify those fields - IM_ASSERT(callback_data.BufSize == edit_state.BufSizeA); - IM_ASSERT(callback_data.Flags == flags); - if (callback_data.CursorPos != utf8_cursor_pos) - edit_state.StbState.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); - if (callback_data.SelectionStart != utf8_selection_start) - edit_state.StbState.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); - if (callback_data.SelectionEnd != utf8_selection_end) - edit_state.StbState.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); - if (callback_data.BufDirty) - { - IM_ASSERT(callback_data.BufTextLen == (int) strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! - edit_state.CurLenW = ImTextStrFromUtf8(edit_state.Text.Data, edit_state.Text.Size, callback_data.Buf, NULL); - edit_state.CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() - edit_state.CursorAnimReset(); - } - } - } - - // Copy back to user buffer - if (is_editable && strcmp(edit_state.TempTextBuffer.Data, buf) != 0) - { - ImStrncpy(buf, edit_state.TempTextBuffer.Data, buf_size); - value_changed = true; - } - } - } - - // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) - if (clear_active_id && g.ActiveId == id) - ClearActiveID(); - - // Render - // Select which buffer we are going to display. When ImGuiInputTextFlags_NoLiveEdit is set 'buf' might still be the old value. We set buf to NULL to prevent accidental usage from now on. - const char *buf_display = (g.ActiveId == id && is_editable) ? edit_state.TempTextBuffer.Data : buf; - buf = NULL; - - RenderNavHighlight(frame_bb, id); - if (!is_multiline) - RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - - const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + size.x, frame_bb.Min.y + size.y); // Not using frame_bb.Max because we have adjusted size - ImVec2 render_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; - ImVec2 text_size(0.f, 0.f); - const bool is_currently_scrolling = (edit_state.Id == id && is_multiline && g.ActiveId == draw_window->GetIDNoKeepAlive("#SCROLLY")); - if (g.ActiveId == id || is_currently_scrolling) - { - edit_state.CursorAnim += io.DeltaTime; - - // This is going to be messy. We need to: - // - Display the text (this alone can be more easily clipped) - // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) - // - Measure text height (for scrollbar) - // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) - // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. - const ImWchar *text_begin = edit_state.Text.Data; - ImVec2 cursor_offset, select_start_offset; - - { - // Count lines + find lines numbers straddling 'cursor' and 'select_start' position. - const ImWchar *searches_input_ptr[2]; - searches_input_ptr[0] = text_begin + edit_state.StbState.cursor; - searches_input_ptr[1] = NULL; - int searches_remaining = 1; - int searches_result_line_number[2] = {-1, -999}; - if (edit_state.StbState.select_start != edit_state.StbState.select_end) - { - searches_input_ptr[1] = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); - searches_result_line_number[1] = -1; - searches_remaining++; - } - - // Iterate all lines to find our line numbers - // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. - searches_remaining += is_multiline ? 1 : 0; - int line_count = 0; - for (const ImWchar *s = text_begin; *s != 0; s++) - if (*s == '\n') - { - line_count++; - if (searches_result_line_number[0] == -1 && s >= searches_input_ptr[0]) - { - searches_result_line_number[0] = line_count; - if (--searches_remaining <= 0) - break; - } - if (searches_result_line_number[1] == -1 && s >= searches_input_ptr[1]) - { - searches_result_line_number[1] = line_count; - if (--searches_remaining <= 0) - break; - } - } - line_count++; - if (searches_result_line_number[0] == -1) - searches_result_line_number[0] = line_count; - if (searches_result_line_number[1] == -1) - searches_result_line_number[1] = line_count; - - // Calculate 2d position by finding the beginning of the line and measuring distance - cursor_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; - cursor_offset.y = searches_result_line_number[0] * g.FontSize; - if (searches_result_line_number[1] >= 0) - { - select_start_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; - select_start_offset.y = searches_result_line_number[1] * g.FontSize; - } - - // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) - if (is_multiline) - text_size = ImVec2(size.x, line_count * g.FontSize); - } - - // Scroll - if (edit_state.CursorFollow) - { - // Horizontal scroll in chunks of quarter width - if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) - { - const float scroll_increment_x = size.x * 0.25f; - if (cursor_offset.x < edit_state.ScrollX) - edit_state.ScrollX = (float) (int) ImMax(0.0f, cursor_offset.x - scroll_increment_x); - else if (cursor_offset.x - size.x >= edit_state.ScrollX) - edit_state.ScrollX = (float) (int) (cursor_offset.x - size.x + scroll_increment_x); - } - else - { - edit_state.ScrollX = 0.0f; - } - - // Vertical scroll - if (is_multiline) - { - float scroll_y = draw_window->Scroll.y; - if (cursor_offset.y - g.FontSize < scroll_y) - scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); - else if (cursor_offset.y - size.y >= scroll_y) - scroll_y = cursor_offset.y - size.y; - draw_window->DC.CursorPos.y += (draw_window->Scroll.y - scroll_y); // To avoid a frame of lag - draw_window->Scroll.y = scroll_y; - render_pos.y = draw_window->DC.CursorPos.y; - } - } - edit_state.CursorFollow = false; - const ImVec2 render_scroll = ImVec2(edit_state.ScrollX, 0.0f); - - // Draw selection - if (edit_state.StbState.select_start != edit_state.StbState.select_end) - { - const ImWchar *text_selected_begin = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); - const ImWchar *text_selected_end = text_begin + ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end); - - float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. - float bg_offy_dn = is_multiline ? 0.0f : 2.0f; - ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg); - ImVec2 rect_pos = render_pos + select_start_offset - render_scroll; - for (const ImWchar *p = text_selected_begin; p < text_selected_end;) - { - if (rect_pos.y > clip_rect.w + g.FontSize) - break; - if (rect_pos.y < clip_rect.y) - { - while (p < text_selected_end) - if (*p++ == '\n') - break; - } - else - { - ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true); - if (rect_size.x <= 0.0f) - rect_size.x = (float) (int) (g.Font->GetCharAdvance((unsigned short) ' ') * 0.50f); // So we can see selected empty lines - ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); - rect.ClipWith(clip_rect); - if (rect.Overlaps(clip_rect)) - draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); - } - rect_pos.x = render_pos.x - render_scroll.x; - rect_pos.y += g.FontSize; - } - } - - draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos - render_scroll, GetColorU32(ImGuiCol_Text), buf_display, buf_display + edit_state.CurLenA, 0.0f, is_multiline ? NULL : &clip_rect); - - // Draw blinking cursor - bool cursor_is_visible = (!g.IO.OptCursorBlink) || (g.InputTextState.CursorAnim <= 0.0f) || fmodf(g.InputTextState.CursorAnim, 1.20f) <= 0.80f; - ImVec2 cursor_screen_pos = render_pos + cursor_offset - render_scroll; - ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); - if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) - draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); - - // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) - if (is_editable) - g.OsImePosRequest = ImVec2(cursor_screen_pos.x - 1, cursor_screen_pos.y - g.FontSize); - } - else - { - // Render text only - const char *buf_end = NULL; - if (is_multiline) - text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_end) * g.FontSize); // We don't need width - draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos, GetColorU32(ImGuiCol_Text), buf_display, buf_end, 0.0f, is_multiline ? NULL : &clip_rect); - } - - if (is_multiline) - { - Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line - EndChildFrame(); - EndGroup(); - } - - if (is_password) - PopFont(); - - // Log as text - if (g.LogEnabled && !is_password) - LogRenderedText(&render_pos, buf_display, NULL); - - if (label_size.x > 0) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - - if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) - return enter_pressed; - else - return value_changed; -} - -bool ImGui::InputText(const char *label, char *buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void *user_data) -{ - IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() - return InputTextEx(label, buf, (int) buf_size, ImVec2(0, 0), flags, callback, user_data); -} - -bool ImGui::InputTextMultiline(const char *label, char *buf, size_t buf_size, const ImVec2 &size, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback, void *user_data) -{ - return InputTextEx(label, buf, (int) buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); -} - -// NB: scalar_format here must be a simple "%xx" format string with no prefix/suffix (unlike the Drag/Slider functions "display_format" argument) -bool ImGui::InputScalarEx(const char *label, ImGuiDataType data_type, void *data_ptr, void *step_ptr, void *step_fast_ptr, const char *scalar_format, ImGuiInputTextFlags extra_flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImVec2 label_size = CalcTextSize(label, NULL, true); - - BeginGroup(); - PushID(label); - const ImVec2 button_sz = ImVec2(GetFrameHeight(), GetFrameHeight()); - if (step_ptr) - PushItemWidth(ImMax(1.0f, CalcItemWidth() - (button_sz.x + style.ItemInnerSpacing.x) * 2)); - - char buf[64]; - DataTypeFormatString(data_type, data_ptr, scalar_format, buf, IM_ARRAYSIZE(buf)); - - bool value_changed = false; - if (!(extra_flags & ImGuiInputTextFlags_CharsHexadecimal)) - extra_flags |= ImGuiInputTextFlags_CharsDecimal; - extra_flags |= ImGuiInputTextFlags_AutoSelectAll; - if (InputText("", buf, IM_ARRAYSIZE(buf), extra_flags)) // PushId(label) + "" gives us the expected ID from outside point of view - value_changed = DataTypeApplyOpFromText(buf, GImGui->InputTextState.InitialText.begin(), data_type, data_ptr, scalar_format); - - // Step buttons - if (step_ptr) - { - PopItemWidth(); - SameLine(0, style.ItemInnerSpacing.x); - if (ButtonEx("-", button_sz, ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups)) - { - DataTypeApplyOp(data_type, '-', data_ptr, g.IO.KeyCtrl && step_fast_ptr ? step_fast_ptr : step_ptr); - value_changed = true; - } - SameLine(0, style.ItemInnerSpacing.x); - if (ButtonEx("+", button_sz, ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups)) - { - DataTypeApplyOp(data_type, '+', data_ptr, g.IO.KeyCtrl && step_fast_ptr ? step_fast_ptr : step_ptr); - value_changed = true; - } - } - PopID(); +ImGuiID ImGui::GetID(int int_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(int_id); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE - if (label_size.x > 0) - { - SameLine(0, style.ItemInnerSpacing.x); - RenderText(ImVec2(window->DC.CursorPos.x, window->DC.CursorPos.y + style.FramePadding.y), label); - ItemSize(label_size, style.FramePadding.y); - } - EndGroup(); +//----------------------------------------------------------------------------- +// [SECTION] INPUTS +//----------------------------------------------------------------------------- +// - GetModForLRModKey() [Internal] +// - FixupKeyChord() [Internal] +// - GetKeyData() [Internal] +// - GetKeyIndex() [Internal] +// - GetKeyName() +// - GetKeyChordName() [Internal] +// - CalcTypematicRepeatAmount() [Internal] +// - GetTypematicRepeatRate() [Internal] +// - GetKeyPressedAmount() [Internal] +// - GetKeyMagnitude2d() [Internal] +//----------------------------------------------------------------------------- +// - UpdateKeyRoutingTable() [Internal] +// - GetRoutingIdFromOwnerId() [Internal] +// - GetShortcutRoutingData() [Internal] +// - CalcRoutingScore() [Internal] +// - SetShortcutRouting() [Internal] +// - TestShortcutRouting() [Internal] +//----------------------------------------------------------------------------- +// - IsKeyDown() +// - IsKeyPressed() +// - IsKeyReleased() +//----------------------------------------------------------------------------- +// - IsMouseDown() +// - IsMouseClicked() +// - IsMouseReleased() +// - IsMouseDoubleClicked() +// - GetMouseClickedCount() +// - IsMouseHoveringRect() [Internal] +// - IsMouseDragPastThreshold() [Internal] +// - IsMouseDragging() +// - GetMousePos() +// - SetMousePos() [Internal] +// - GetMousePosOnOpeningCurrentPopup() +// - IsMousePosValid() +// - IsAnyMouseDown() +// - GetMouseDragDelta() +// - ResetMouseDragDelta() +// - GetMouseCursor() +// - SetMouseCursor() +//----------------------------------------------------------------------------- +// - UpdateAliasKey() +// - GetMergedModsFromKeys() +// - UpdateKeyboardInputs() +// - UpdateMouseInputs() +//----------------------------------------------------------------------------- +// - LockWheelingWindow [Internal] +// - FindBestWheelingWindow [Internal] +// - UpdateMouseWheel() [Internal] +//----------------------------------------------------------------------------- +// - SetNextFrameWantCaptureKeyboard() +// - SetNextFrameWantCaptureMouse() +//----------------------------------------------------------------------------- +// - GetInputSourceName() [Internal] +// - DebugPrintInputEvent() [Internal] +// - UpdateInputEvents() [Internal] +//----------------------------------------------------------------------------- +// - GetKeyOwner() [Internal] +// - TestKeyOwner() [Internal] +// - SetKeyOwner() [Internal] +// - SetItemKeyOwner() [Internal] +// - Shortcut() [Internal] +//----------------------------------------------------------------------------- - return value_changed; +static ImGuiKeyChord GetModForLRModKey(ImGuiKey key) +{ + if (key == ImGuiKey_LeftCtrl || key == ImGuiKey_RightCtrl) + return ImGuiMod_Ctrl; + if (key == ImGuiKey_LeftShift || key == ImGuiKey_RightShift) + return ImGuiMod_Shift; + if (key == ImGuiKey_LeftAlt || key == ImGuiKey_RightAlt) + return ImGuiMod_Alt; + if (key == ImGuiKey_LeftSuper || key == ImGuiKey_RightSuper) + return ImGuiMod_Super; + return ImGuiMod_None; } -bool ImGui::InputFloat(const char *label, float *v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags extra_flags) +ImGuiKeyChord ImGui::FixupKeyChord(ImGuiKeyChord key_chord) { - char display_format[16]; - if (decimal_precision < 0) - strcpy(display_format, "%f"); // Ideally we'd have a minimum decimal precision of 1 to visually denote that this is a float, while hiding non-significant digits? %f doesn't have a minimum of 1 - else - ImFormatString(display_format, IM_ARRAYSIZE(display_format), "%%.%df", decimal_precision); - return InputScalarEx(label, ImGuiDataType_Float, (void *) v, (void *) (step > 0.0f ? &step : NULL), (void *) (step_fast > 0.0f ? &step_fast : NULL), display_format, extra_flags); + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord |= GetModForLRModKey(key); + return key_chord; } -bool ImGui::InputInt(const char *label, int *v, int step, int step_fast, ImGuiInputTextFlags extra_flags) +ImGuiKeyData* ImGui::GetKeyData(ImGuiContext* ctx, ImGuiKey key) { - // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. - const char *scalar_format = (extra_flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; - return InputScalarEx(label, ImGuiDataType_Int, (void *) v, (void *) (step > 0.0f ? &step : NULL), (void *) (step_fast > 0.0f ? &step_fast : NULL), scalar_format, extra_flags); + ImGuiContext& g = *ctx; + + // Special storage location for mods + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT(key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_NamedKey_END); + if (IsLegacyKey(key) && g.IO.KeyMap[key] != -1) + key = (ImGuiKey)g.IO.KeyMap[key]; // Remap native->imgui or imgui->native +#else + IM_ASSERT(IsNamedKey(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend & user code."); +#endif + return &g.IO.KeysData[key - ImGuiKey_KeysData_OFFSET]; } -bool ImGui::InputFloatN(const char *label, float *v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags) +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO +// Formally moved to obsolete section in 1.90.5 in spite of documented as obsolete since 1.87 +ImGuiKey ImGui::GetKeyIndex(ImGuiKey key) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiContext& g = *GImGui; + IM_ASSERT(IsNamedKey(key)); + const ImGuiKeyData* key_data = GetKeyData(key); + return (ImGuiKey)(key_data - g.IO.KeysData); +} +#endif - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= InputFloat("##v", &v[i], 0, 0, decimal_precision, extra_flags); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); +// Those names a provided for debugging purpose and are not meant to be saved persistently not compared. +static const char* const GKeyNames[] = +{ + "Tab", "LeftArrow", "RightArrow", "UpArrow", "DownArrow", "PageUp", "PageDown", + "Home", "End", "Insert", "Delete", "Backspace", "Space", "Enter", "Escape", + "LeftCtrl", "LeftShift", "LeftAlt", "LeftSuper", "RightCtrl", "RightShift", "RightAlt", "RightSuper", "Menu", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", + "Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket", + "Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen", + "Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6", + "Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply", + "KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual", + "AppBack", "AppForward", + "GamepadStart", "GamepadBack", + "GamepadFaceLeft", "GamepadFaceRight", "GamepadFaceUp", "GamepadFaceDown", + "GamepadDpadLeft", "GamepadDpadRight", "GamepadDpadUp", "GamepadDpadDown", + "GamepadL1", "GamepadR1", "GamepadL2", "GamepadR2", "GamepadL3", "GamepadR3", + "GamepadLStickLeft", "GamepadLStickRight", "GamepadLStickUp", "GamepadLStickDown", + "GamepadRStickLeft", "GamepadRStickRight", "GamepadRStickUp", "GamepadRStickDown", + "MouseLeft", "MouseRight", "MouseMiddle", "MouseX1", "MouseX2", "MouseWheelX", "MouseWheelY", + "ModCtrl", "ModShift", "ModAlt", "ModSuper", // ReservedForModXXX are showing the ModXXX names. +}; +IM_STATIC_ASSERT(ImGuiKey_NamedKey_COUNT == IM_ARRAYSIZE(GKeyNames)); - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); +const char* ImGui::GetKeyName(ImGuiKey key) +{ + if (key == ImGuiKey_None) + return "None"; +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + IM_ASSERT(IsNamedKeyOrMod(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); +#else + ImGuiContext& g = *GImGui; + if (IsLegacyKey(key)) + { + if (g.IO.KeyMap[key] == -1) + return "N/A"; + IM_ASSERT(IsNamedKey((ImGuiKey)g.IO.KeyMap[key])); + key = (ImGuiKey)g.IO.KeyMap[key]; + } +#endif + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + if (!IsNamedKey(key)) + return "Unknown"; + + return GKeyNames[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Return untranslated names: on macOS, Cmd key will show as Ctrl, Ctrl key will show as super. +// Lifetime of return value: valid until next call to same function. +const char* ImGui::GetKeyChordName(ImGuiKeyChord key_chord) +{ + ImGuiContext& g = *GImGui; + + const ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord &= ~GetModForLRModKey(key); // Return "Ctrl+LeftShift" instead of "Ctrl+Shift+LeftShift" + ImFormatString(g.TempKeychordName, IM_ARRAYSIZE(g.TempKeychordName), "%s%s%s%s%s", + (key_chord & ImGuiMod_Ctrl) ? "Ctrl+" : "", + (key_chord & ImGuiMod_Shift) ? "Shift+" : "", + (key_chord & ImGuiMod_Alt) ? "Alt+" : "", + (key_chord & ImGuiMod_Super) ? "Super+" : "", + (key != ImGuiKey_None || key_chord == ImGuiKey_None) ? GetKeyName(key) : ""); + size_t len; + if (key == ImGuiKey_None && key_chord != 0) + if ((len = strlen(g.TempKeychordName)) != 0) // Remove trailing '+' + g.TempKeychordName[len - 1] = 0; + return g.TempKeychordName; +} + +// t0 = previous time (e.g.: g.Time - g.IO.DeltaTime) +// t1 = current time (e.g.: g.Time) +// An event is triggered at: +// t = 0.0f t = repeat_delay, t = repeat_delay + repeat_rate*N +int ImGui::CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate) +{ + if (t1 == 0.0f) + return 1; + if (t0 >= t1) + return 0; + if (repeat_rate <= 0.0f) + return (t0 < repeat_delay) && (t1 >= repeat_delay); + const int count_t0 = (t0 < repeat_delay) ? -1 : (int)((t0 - repeat_delay) / repeat_rate); + const int count_t1 = (t1 < repeat_delay) ? -1 : (int)((t1 - repeat_delay) / repeat_rate); + const int count = count_t1 - count_t0; + return count; +} + +void ImGui::GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate) +{ + ImGuiContext& g = *GImGui; + switch (flags & ImGuiInputFlags_RepeatRateMask_) + { + case ImGuiInputFlags_RepeatRateNavMove: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.80f; return; + case ImGuiInputFlags_RepeatRateNavTweak: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.30f; return; + case ImGuiInputFlags_RepeatRateDefault: default: *repeat_delay = g.IO.KeyRepeatDelay * 1.00f; *repeat_rate = g.IO.KeyRepeatRate * 1.00f; return; + } +} - return value_changed; +// Return value representing the number of presses in the last time period, for the given repeat rate +// (most often returns 0 or 1. The result is generally only >1 when RepeatRate is smaller than DeltaTime, aka large DeltaTime or fast RepeatRate) +int ImGui::GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float repeat_rate) +{ + ImGuiContext& g = *GImGui; + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return 0; + const float t = key_data->DownDuration; + return CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, repeat_delay, repeat_rate); } -bool ImGui::InputFloat2(const char *label, float v[2], int decimal_precision, ImGuiInputTextFlags extra_flags) +// Return 2D vector representing the combination of four cardinal direction, with analog value support (for e.g. ImGuiKey_GamepadLStick* values). +ImVec2 ImGui::GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down) { - return InputFloatN(label, v, 2, decimal_precision, extra_flags); + return ImVec2( + GetKeyData(key_right)->AnalogValue - GetKeyData(key_left)->AnalogValue, + GetKeyData(key_down)->AnalogValue - GetKeyData(key_up)->AnalogValue); } -bool ImGui::InputFloat3(const char *label, float v[3], int decimal_precision, ImGuiInputTextFlags extra_flags) +// Rewrite routing data buffers to strip old entries + sort by key to make queries not touch scattered data. +// Entries D,A,B,B,A,C,B --> A,A,B,B,B,C,D +// Index A:1 B:2 C:5 D:0 --> A:0 B:2 C:5 D:6 +// See 'Metrics->Key Owners & Shortcut Routing' to visualize the result of that operation. +static void ImGui::UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt) { - return InputFloatN(label, v, 3, decimal_precision, extra_flags); + ImGuiContext& g = *GImGui; + rt->EntriesNext.resize(0); + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + const int new_routing_start_idx = rt->EntriesNext.Size; + ImGuiKeyRoutingData* routing_entry; + for (int old_routing_idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; old_routing_idx != -1; old_routing_idx = routing_entry->NextEntryIndex) + { + routing_entry = &rt->Entries[old_routing_idx]; + routing_entry->RoutingCurrScore = routing_entry->RoutingNextScore; + routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry + routing_entry->RoutingNext = ImGuiKeyOwner_NoOwner; + routing_entry->RoutingNextScore = 255; + if (routing_entry->RoutingCurr == ImGuiKeyOwner_NoOwner) + continue; + rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer + + // Apply routing to owner if there's no owner already (RoutingCurr == None at this point) + // This is the result of previous frame's SetShortcutRouting() call. + if (routing_entry->Mods == g.IO.KeyMods) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + { + owner_data->OwnerCurr = routing_entry->RoutingCurr; + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X) via Routing\n", GetKeyName(key), routing_entry->RoutingCurr); + } + } + } + + // Rewrite linked-list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = (ImGuiKeyRoutingIndex)(new_routing_start_idx < rt->EntriesNext.Size ? new_routing_start_idx : -1); + for (int n = new_routing_start_idx; n < rt->EntriesNext.Size; n++) + rt->EntriesNext[n].NextEntryIndex = (ImGuiKeyRoutingIndex)((n + 1 < rt->EntriesNext.Size) ? n + 1 : -1); + } + rt->Entries.swap(rt->EntriesNext); // Swap new and old indexes } -bool ImGui::InputFloat4(const char *label, float v[4], int decimal_precision, ImGuiInputTextFlags extra_flags) +// owner_id may be None/Any, but routing_id needs to be always be set, so we default to GetCurrentFocusScope(). +static inline ImGuiID GetRoutingIdFromOwnerId(ImGuiID owner_id) { - return InputFloatN(label, v, 4, decimal_precision, extra_flags); + ImGuiContext& g = *GImGui; + return (owner_id != ImGuiKeyOwner_NoOwner && owner_id != ImGuiKeyOwner_Any) ? owner_id : g.CurrentFocusScopeId; } -bool ImGui::InputIntN(const char *label, int *v, int components, ImGuiInputTextFlags extra_flags) +ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; + // Majority of shortcuts will be Key + any number of Mods + // We accept _Single_ mod with ImGuiKey_None. + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl | ImGuiMod_Shift); // Legal + // - Shortcut(ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift); // Not legal + ImGuiContext& g = *GImGui; + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + ImGuiKeyRoutingData* routing_data; + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + IM_ASSERT(IsNamedKey(key)); + + // Get (in the majority of case, the linked list will have one element so this should be 2 reads. + // Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame). + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; idx = routing_data->NextEntryIndex) + { + routing_data = &rt->Entries[idx]; + if (routing_data->Mods == mods) + return routing_data; + } + + // Add to linked-list + ImGuiKeyRoutingIndex routing_data_idx = (ImGuiKeyRoutingIndex)rt->Entries.Size; + rt->Entries.push_back(ImGuiKeyRoutingData()); + routing_data = &rt->Entries[routing_data_idx]; + routing_data->Mods = (ImU16)mods; + routing_data->NextEntryIndex = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; // Setup linked list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = routing_data_idx; + return routing_data; +} + +// Current score encoding (lower is highest priority): +// - 0: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverActive +// - 1: ImGuiInputFlags_ActiveItem or ImGuiInputFlags_RouteFocused (if item active) +// - 2: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused +// - 3+: ImGuiInputFlags_RouteFocused (if window in focus-stack) +// - 254: ImGuiInputFlags_RouteGlobal +// - 255: never route +// 'flags' should include an explicit routing policy +static int CalcRoutingScore(ImGuiID focus_scope_id, ImGuiID owner_id, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiInputFlags_RouteFocused) + { + // ActiveID gets top priority + // (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it) + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + + // Score based on distance to focused window (lower is better) + // Assuming both windows are submitting a routing request, + // - When Window....... is focused -> Window scores 3 (best), Window/ChildB scores 255 (no match) + // - When Window/ChildB is focused -> Window scores 4, Window/ChildB scores 3 (best) + // Assuming only WindowA is submitting a routing request, + // - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score. + // This essentially follow the window->ParentWindowForFocusRoute chain. + if (focus_scope_id == 0) + return 255; + for (int index_in_focus_path = 0; index_in_focus_path < g.NavFocusRoute.Size; index_in_focus_path++) + if (g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id) + return 3 + index_in_focus_path; + return 255; + } + else if (flags & ImGuiInputFlags_RouteActive) + { + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + return 255; + } + else if (flags & ImGuiInputFlags_RouteGlobal) + { + if (flags & ImGuiInputFlags_RouteOverActive) + return 0; + if (flags & ImGuiInputFlags_RouteOverFocused) + return 2; + return 254; + } + IM_ASSERT(0); + return 0; +} + +// - We need this to filter some Shortcut() routes when an item e.g. an InputText() is active +// e.g. ImGuiKey_G won't be considered a shortcut when item is active, but ImGuiMod|ImGuiKey_G can be. +// - This is also used by UpdateInputEvents() to avoid trickling in the most common case of e.g. pressing ImGuiKey_G also emitting a G character. +static bool IsKeyChordPotentiallyCharInput(ImGuiKeyChord key_chord) +{ + // Mimic 'ignore_char_inputs' logic in InputText() + ImGuiContext& g = *GImGui; + + // When the right mods are pressed it cannot be a char input so we won't filter the shortcut out. + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + const bool ignore_char_inputs = ((mods & ImGuiMod_Ctrl) && !(mods & ImGuiMod_Alt)) || (g.IO.ConfigMacOSXBehaviors && (mods & ImGuiMod_Ctrl)); + if (ignore_char_inputs) + return false; + + // Return true for A-Z, 0-9 and other keys associated to char inputs. Other keys such as F1-F12 won't be filtered. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + return false; + return g.KeysMayBeCharInput.TestBit(key); +} + +// Request a desired route for an input chord (key + mods). +// Return true if the route is available this frame. +// - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state. +// (Conceptually this does a "Submit for next frame" + "Test for current frame". +// As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.) +bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive; // IMPORTANT: This is the default for SetShortcutRouting() but NOT Shortcut() + else + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiInputFlags_RouteTypeMask_)); // Check that only 1 routing flag is used + IM_ASSERT(owner_id != ImGuiKeyOwner_Any && owner_id != ImGuiKeyOwner_NoOwner); + if (flags & (ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused)) + IM_ASSERT(flags & ImGuiInputFlags_RouteGlobal); + + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + key_chord = FixupKeyChord(key_chord); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInShortcutRouting == key_chord) + IM_DEBUG_BREAK(); + + if (flags & ImGuiInputFlags_RouteUnlessBgFocused) + if (g.NavWindow == NULL) + return false; + + // Note how ImGuiInputFlags_RouteAlways won't set routing and thus won't set owner. May want to rework this? + if (flags & ImGuiInputFlags_RouteAlways) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> always, no register\n", GetKeyChordName(key_chord), flags, owner_id); + return true; + } - ImGuiContext &g = *GImGui; - bool value_changed = false; - BeginGroup(); - PushID(label); - PushMultiItemsWidths(components); - for (int i = 0; i < components; i++) - { - PushID(i); - value_changed |= InputInt("##v", &v[i], 0, 0, extra_flags); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); + // Specific culling when there's an active item. + if (g.ActiveId != 0 && g.ActiveId != owner_id) + { + if (flags & ImGuiInputFlags_RouteActive) + return false; + + // Cull shortcuts with no modifiers when it could generate a character. + // e.g. Shortcut(ImGuiKey_G) also generates 'g' character, should not trigger when InputText() is active. + // but Shortcut(Ctrl+G) should generally trigger when InputText() is active. + // TL;DR: lettered shortcut with no mods or with only Alt mod will not trigger while an item reading text input is active. + // (We cannot filter based on io.InputQueueCharacters[] contents because of trickling and key<>chars submission order are undefined) + if (g.IO.WantTextInput && IsKeyChordPotentiallyCharInput(key_chord)) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> filtered as potential char input\n", GetKeyChordName(key_chord), flags, owner_id); + return false; + } + + // ActiveIdUsingAllKeyboardKeys trumps all for ActiveId + if ((flags & ImGuiInputFlags_RouteOverActive) == 0 && g.ActiveIdUsingAllKeyboardKeys) + { + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey((ImGuiKey)(key_chord & ImGuiMod_Mask_)); + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + } + } - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); + // Where do we evaluate route for? + ImGuiID focus_scope_id = g.CurrentFocusScopeId; + if (flags & ImGuiInputFlags_RouteFromRootWindow) + focus_scope_id = g.CurrentWindow->RootWindow->ID; // See PushFocusScope() call in Begin() + + const int score = CalcRoutingScore(focus_scope_id, owner_id, flags); + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> score %d\n", GetKeyChordName(key_chord), flags, owner_id, score); + if (score == 255) + return false; + + // Submit routing for NEXT frame (assuming score is sufficient) + // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <). + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); + //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore); + if (score < routing_data->RoutingNextScore) + { + routing_data->RoutingNext = owner_id; + routing_data->RoutingNextScore = (ImU8)score; + } - return value_changed; + // Return routing state for CURRENT frame + if (routing_data->RoutingCurr == owner_id) + IMGUI_DEBUG_LOG_INPUTROUTING("--> granting current route\n"); + return routing_data->RoutingCurr == owner_id; } -bool ImGui::InputInt2(const char *label, int v[2], ImGuiInputTextFlags extra_flags) +// Currently unused by core (but used by tests) +// Note: this cannot be turned into GetShortcutRouting() because we do the owner_id->routing_id translation, name would be more misleading. +bool ImGui::TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id) { - return InputIntN(label, v, 2, extra_flags); + const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); + key_chord = FixupKeyChord(key_chord); + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); // FIXME: Could avoid creating entry. + return routing_data->RoutingCurr == routing_id; } -bool ImGui::InputInt3(const char *label, int v[3], ImGuiInputTextFlags extra_flags) +// Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes. +// Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey_A) (>= 1.87) +bool ImGui::IsKeyDown(ImGuiKey key) { - return InputIntN(label, v, 3, extra_flags); + return IsKeyDown(key, ImGuiKeyOwner_Any); } -bool ImGui::InputInt4(const char *label, int v[4], ImGuiInputTextFlags extra_flags) +bool ImGui::IsKeyDown(ImGuiKey key, ImGuiID owner_id) { - return InputIntN(label, v, 4, extra_flags); + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; } -static float CalcMaxPopupHeightFromItemCount(int items_count) +bool ImGui::IsKeyPressed(ImGuiKey key, bool repeat) { - ImGuiContext &g = *GImGui; - if (items_count <= 0) - return FLT_MAX; - return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); + return IsKeyPressed(key, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); } -bool ImGui::BeginCombo(const char *label, const char *preview_value, ImGuiComboFlags flags) +// Important: unless legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat. +bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id) { - // Always consume the SetNextWindowSizeConstraint() call in our early return paths - ImGuiContext &g = *GImGui; - ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond; - g.NextWindowData.SizeConstraintCond = 0; - - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - const float w = CalcItemWidth(); - - const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) - return false; + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = key_data->DownDuration; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function! + if (flags & (ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_)) // Setting any _RepeatXXX option enables _Repeat + flags |= ImGuiInputFlags_Repeat; - bool hovered, held; - bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held); - bool popup_open = IsPopupOpen(id); - - const float arrow_size = GetFrameHeight(); - const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f)); - RenderNavHighlight(frame_bb, id); - RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - RenderFrame(ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32(popup_open || hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button), true, style.FrameRounding); // FIXME-ROUNDING - RenderTriangle(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); - if (preview_value != NULL) - RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f, 0.0f)); - if (label_size.x > 0) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - - if ((pressed || g.NavActivateId == id) && !popup_open) - { - if (window->DC.NavLayerCurrent == 0) - window->NavLastIds[0] = id; - OpenPopupEx(id); - popup_open = true; - } - - if (!popup_open) - return false; - - if (backup_next_window_size_constraint) - { - g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint; - g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); - } - else - { - if ((flags & ImGuiComboFlags_HeightMask_) == 0) - flags |= ImGuiComboFlags_HeightRegular; - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one - int popup_max_height_in_items = -1; - if (flags & ImGuiComboFlags_HeightRegular) - popup_max_height_in_items = 8; - else if (flags & ImGuiComboFlags_HeightSmall) - popup_max_height_in_items = 4; - else if (flags & ImGuiComboFlags_HeightLarge) - popup_max_height_in_items = 20; - SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); - } - - char name[16]; - ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth - - // Peak into expected window size so we can position it - if (ImGuiWindow *popup_window = FindWindowByName(name)) - if (popup_window->WasActive) - { - ImVec2 size_contents = CalcSizeContents(popup_window); - ImVec2 size_expected = CalcSizeAfterConstraint(popup_window, CalcSizeAutoFit(popup_window, size_contents)); - if (flags & ImGuiComboFlags_PopupAlignLeft) - popup_window->AutoPosLastDirection = ImGuiDir_Left; - ImVec2 pos = FindBestWindowPosForPopup(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, frame_bb, ImGuiPopupPositionPolicy_ComboBox); - SetNextWindowPos(pos); - } - - ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; - if (!Begin(name, NULL, window_flags)) - { - EndPopup(); - IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above - return false; - } - - // Horizontally align ourselves with the framed text - if (style.FramePadding.x != style.WindowPadding.x) - Indent(style.FramePadding.x - style.WindowPadding.x); - - return true; -} - -void ImGui::EndCombo() -{ - const ImGuiStyle &style = GImGui->Style; - if (style.FramePadding.x != style.WindowPadding.x) - Unindent(style.FramePadding.x - style.WindowPadding.x); - EndPopup(); -} - -// Old API, prefer using BeginCombo() nowadays if you can. -bool ImGui::Combo(const char *label, int *current_item, bool (*items_getter)(void *, int, const char **), void *data, int items_count, int popup_max_height_in_items) -{ - ImGuiContext &g = *GImGui; - - const char *preview_text = NULL; - if (*current_item >= 0 && *current_item < items_count) - items_getter(data, *current_item, &preview_text); - - // The old Combo() API exposed "popup_max_height_in_items", however the new more general BeginCombo() API doesn't, so we emulate it here. - if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond) - { - float popup_max_height = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); - SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, popup_max_height)); - } - - if (!BeginCombo(label, preview_text, 0)) - return false; - - // Display items - // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) - bool value_changed = false; - for (int i = 0; i < items_count; i++) - { - PushID((void *) (intptr_t) i); - const bool item_selected = (i == *current_item); - const char *item_text; - if (!items_getter(data, i, &item_text)) - item_text = "*Unknown item*"; - if (Selectable(item_text, item_selected)) - { - value_changed = true; - *current_item = i; - } - if (item_selected) - SetItemDefaultFocus(); - PopID(); - } - - EndCombo(); - return value_changed; -} - -static bool Items_ArrayGetter(void *data, int idx, const char **out_text) -{ - const char *const *items = (const char *const *) data; - if (out_text) - *out_text = items[idx]; - return true; -} - -static bool Items_SingleStringGetter(void *data, int idx, const char **out_text) -{ - // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. - const char *items_separated_by_zeros = (const char *) data; - int items_count = 0; - const char *p = items_separated_by_zeros; - while (*p) - { - if (idx == items_count) - break; - p += strlen(p) + 1; - items_count++; - } - if (!*p) - return false; - if (out_text) - *out_text = p; - return true; -} - -// Combo box helper allowing to pass an array of strings. -bool ImGui::Combo(const char *label, int *current_item, const char *const items[], int items_count, int height_in_items) -{ - const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void *) items, items_count, height_in_items); - return value_changed; -} - -// Combo box helper allowing to pass all items in a single string. -bool ImGui::Combo(const char *label, int *current_item, const char *items_separated_by_zeros, int height_in_items) -{ - int items_count = 0; - const char *p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open - while (*p) - { - p += strlen(p) + 1; - items_count++; - } - bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void *) items_separated_by_zeros, items_count, height_in_items); - return value_changed; -} - -// Tip: pass an empty label (e.g. "##dummy") then you can use the space to draw other text or image. -// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID. -bool ImGui::Selectable(const char *label, bool selected, ImGuiSelectableFlags flags, const ImVec2 &size_arg) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) // FIXME-OPT: Avoid if vertically clipped. - PopClipRect(); - - ImGuiID id = window->GetID(label); - ImVec2 label_size = CalcTextSize(label, NULL, true); - ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); - ImVec2 pos = window->DC.CursorPos; - pos.y += window->DC.CurrentLineTextBaseOffset; - ImRect bb(pos, pos + size); - ItemSize(bb); - - // Fill horizontal space. - ImVec2 window_padding = window->WindowPadding; - float max_x = (flags & ImGuiSelectableFlags_SpanAllColumns) ? GetWindowContentRegionMax().x : GetContentRegionMax().x; - float w_draw = ImMax(label_size.x, window->Pos.x + max_x - window_padding.x - window->DC.CursorPos.x); - ImVec2 size_draw((size_arg.x != 0 && !(flags & ImGuiSelectableFlags_DrawFillAvailWidth)) ? size_arg.x : w_draw, size_arg.y != 0.0f ? size_arg.y : size.y); - ImRect bb_with_spacing(pos, pos + size_draw); - if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) - bb_with_spacing.Max.x += window_padding.x; - - // Selectables are tightly packed together, we extend the box to cover spacing between selectable. - float spacing_L = (float) (int) (style.ItemSpacing.x * 0.5f); - float spacing_U = (float) (int) (style.ItemSpacing.y * 0.5f); - float spacing_R = style.ItemSpacing.x - spacing_L; - float spacing_D = style.ItemSpacing.y - spacing_U; - bb_with_spacing.Min.x -= spacing_L; - bb_with_spacing.Min.y -= spacing_U; - bb_with_spacing.Max.x += spacing_R; - bb_with_spacing.Max.y += spacing_D; - if (!ItemAdd(bb_with_spacing, (flags & ImGuiSelectableFlags_Disabled) ? 0 : id)) - { - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) - PushColumnClipRect(); - return false; - } - - ImGuiButtonFlags button_flags = 0; - if (flags & ImGuiSelectableFlags_Menu) - button_flags |= ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_NoHoldingActiveID; - if (flags & ImGuiSelectableFlags_MenuItem) - button_flags |= ImGuiButtonFlags_PressedOnRelease; - if (flags & ImGuiSelectableFlags_Disabled) - button_flags |= ImGuiButtonFlags_Disabled; - if (flags & ImGuiSelectableFlags_AllowDoubleClick) - button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; - bool hovered, held; - bool pressed = ButtonBehavior(bb_with_spacing, id, &hovered, &held, button_flags); - if (flags & ImGuiSelectableFlags_Disabled) - selected = false; - - // Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets) - if (pressed || hovered) // && (g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f)) - if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerActiveMask) - { - g.NavDisableHighlight = true; - SetNavID(id, window->DC.NavLayerCurrent); - } - - // Render - if (hovered || selected) - { - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : - ImGuiCol_Header); - RenderFrame(bb_with_spacing.Min, bb_with_spacing.Max, col, false, 0.0f); - RenderNavHighlight(bb_with_spacing, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); - } - - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) - { - PushColumnClipRect(); - bb_with_spacing.Max.x -= (GetContentRegionMax().x - max_x); - } - - if (flags & ImGuiSelectableFlags_Disabled) - PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); - RenderTextClipped(bb.Min, bb_with_spacing.Max, label, NULL, &label_size, ImVec2(0.0f, 0.0f)); - if (flags & ImGuiSelectableFlags_Disabled) - PopStyleColor(); - - // Automatically close popups - if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) - CloseCurrentPopup(); - return pressed; -} - -bool ImGui::Selectable(const char *label, bool *p_selected, ImGuiSelectableFlags flags, const ImVec2 &size_arg) -{ - if (Selectable(label, *p_selected, flags, size_arg)) - { - *p_selected = !*p_selected; - return true; - } - return false; -} - -// Helper to calculate the size of a listbox and display a label on the right. -// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty" -bool ImGui::ListBoxHeader(const char *label, const ImVec2 &size_arg) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - const ImGuiStyle &style = GetStyle(); - const ImGuiID id = GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - - // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. - ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y); - ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); - ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); - ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy. - - BeginGroup(); - if (label_size.x > 0) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - - BeginChildFrame(id, frame_bb.GetSize()); - return true; -} - -bool ImGui::ListBoxHeader(const char *label, int items_count, int height_in_items) -{ - // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. - // However we don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size. - // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution. - if (height_in_items < 0) - height_in_items = ImMin(items_count, 7); - float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f); - - // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild(). - ImVec2 size; - size.x = 0.0f; - size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y; - return ListBoxHeader(label, size); -} - -void ImGui::ListBoxFooter() -{ - ImGuiWindow *parent_window = GetCurrentWindow()->ParentWindow; - const ImRect bb = parent_window->DC.LastItemRect; - const ImGuiStyle &style = GetStyle(); - - EndChildFrame(); - - // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect) - // We call SameLine() to restore DC.CurrentLine* data - SameLine(); - parent_window->DC.CursorPos = bb.Min; - ItemSize(bb, style.FramePadding.y); - EndGroup(); -} - -bool ImGui::ListBox(const char *label, int *current_item, const char *const items[], int items_count, int height_items) -{ - const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void *) items, items_count, height_items); - return value_changed; -} - -bool ImGui::ListBox(const char *label, int *current_item, bool (*items_getter)(void *, int, const char **), void *data, int items_count, int height_in_items) -{ - if (!ListBoxHeader(label, items_count, height_in_items)) - return false; - - // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper. - bool value_changed = false; - ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - { - const bool item_selected = (i == *current_item); - const char *item_text; - if (!items_getter(data, i, &item_text)) - item_text = "*Unknown item*"; - - PushID(i); - if (Selectable(item_text, item_selected)) - { - *current_item = i; - value_changed = true; - } - if (item_selected) - SetItemDefaultFocus(); - PopID(); - } - ListBoxFooter(); - return value_changed; -} - -bool ImGui::MenuItem(const char *label, const char *shortcut, bool selected, bool enabled) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - ImGuiStyle &style = g.Style; - ImVec2 pos = window->DC.CursorPos; - ImVec2 label_size = CalcTextSize(label, NULL, true); - - ImGuiSelectableFlags flags = ImGuiSelectableFlags_MenuItem | (enabled ? 0 : ImGuiSelectableFlags_Disabled); - bool pressed; - if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) - { - // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful - // Note that in this situation we render neither the shortcut neither the selected tick mark - float w = label_size.x; - window->DC.CursorPos.x += (float) (int) (style.ItemSpacing.x * 0.5f); - PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); - pressed = Selectable(label, false, flags, ImVec2(w, 0.0f)); - PopStyleVar(); - window->DC.CursorPos.x += (float) (int) (style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). - } - else - { - ImVec2 shortcut_size = shortcut ? CalcTextSize(shortcut, NULL) : ImVec2(0.0f, 0.0f); - float w = window->MenuColumns.DeclColumns(label_size.x, shortcut_size.x, (float) (int) (g.FontSize * 1.20f)); // Feedback for next frame - float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); - pressed = Selectable(label, false, flags | ImGuiSelectableFlags_DrawFillAvailWidth, ImVec2(w, 0.0f)); - if (shortcut_size.x > 0.0f) - { - PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); - RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false); - PopStyleColor(); - } - if (selected) - RenderCheckMark(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled), g.FontSize * 0.866f); - } - return pressed; -} - -bool ImGui::MenuItem(const char *label, const char *shortcut, bool *p_selected, bool enabled) -{ - if (MenuItem(label, shortcut, p_selected ? *p_selected : false, enabled)) - { - if (p_selected) - *p_selected = !*p_selected; - return true; - } - return false; -} - -bool ImGui::BeginMainMenuBar() -{ - ImGuiContext &g = *GImGui; - SetNextWindowPos(ImVec2(0.0f, 0.0f)); - SetNextWindowSize(ImVec2(g.IO.DisplaySize.x, g.FontBaseSize + g.Style.FramePadding.y * 2.0f)); - PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); - if (!Begin("##MainMenuBar", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar) || !BeginMenuBar()) - { - End(); - PopStyleVar(2); - return false; - } - g.CurrentWindow->DC.MenuBarOffsetX += g.Style.DisplaySafeAreaPadding.x; - return true; -} - -void ImGui::EndMainMenuBar() -{ - EndMenuBar(); - - // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window - ImGuiContext &g = *GImGui; - if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0) - FocusFrontMostActiveWindow(g.NavWindow); - - End(); - PopStyleVar(2); -} - -bool ImGui::BeginMenuBar() -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - if (!(window->Flags & ImGuiWindowFlags_MenuBar)) - return false; - - IM_ASSERT(!window->DC.MenuBarAppending); - BeginGroup(); // Save position - PushID("##menubar"); - - // We don't clip with regular window clipping rectangle as it is already set to the area below. However we clip with window full rect. - // We remove 1 worth of rounding to Max.x to that text in long menus don't tend to display over the lower-right rounded area, which looks particularly glitchy. - ImRect bar_rect = window->MenuBarRect(); - ImRect clip_rect(ImFloor(bar_rect.Min.x + 0.5f), ImFloor(bar_rect.Min.y + window->WindowBorderSize + 0.5f), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding) + 0.5f), ImFloor(bar_rect.Max.y + 0.5f)); - clip_rect.ClipWith(window->WindowRectClipped); - PushClipRect(clip_rect.Min, clip_rect.Max, false); - - window->DC.CursorPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffsetX, bar_rect.Min.y); // + g.Style.FramePadding.y); - window->DC.LayoutType = ImGuiLayoutType_Horizontal; - window->DC.NavLayerCurrent++; - window->DC.NavLayerCurrentMask <<= 1; - window->DC.MenuBarAppending = true; - AlignTextToFramePadding(); - return true; -} - -void ImGui::EndMenuBar() -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - ImGuiContext &g = *GImGui; - - // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. - if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) - { - ImGuiWindow *nav_earliest_child = g.NavWindow; - while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) - nav_earliest_child = nav_earliest_child->ParentWindow; - if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && g.NavMoveRequestForward == ImGuiNavForward_None) - { - // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. - // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth the hassle/cost) - IM_ASSERT(window->DC.NavLayerActiveMaskNext & 0x02); // Sanity check - FocusWindow(window); - SetNavIDAndMoveMouse(window->NavLastIds[1], 1, window->NavRectRel[1]); - g.NavLayer = 1; - g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. - g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; - NavMoveRequestCancel(); - } - } - - IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); - IM_ASSERT(window->DC.MenuBarAppending); - PopClipRect(); - PopID(); - window->DC.MenuBarOffsetX = window->DC.CursorPos.x - window->MenuBarRect().Min.x; - window->DC.GroupStack.back().AdvanceCursor = false; - EndGroup(); - window->DC.LayoutType = ImGuiLayoutType_Vertical; - window->DC.NavLayerCurrent--; - window->DC.NavLayerCurrentMask >>= 1; - window->DC.MenuBarAppending = false; -} - -bool ImGui::BeginMenu(const char *label, bool enabled) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const ImGuiID id = window->GetID(label); - - ImVec2 label_size = CalcTextSize(label, NULL, true); - - bool pressed; - bool menu_is_open = IsPopupOpen(id); - bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].OpenParentId == window->IDStack.back()); - ImGuiWindow *backed_nav_window = g.NavWindow; - if (menuset_is_open) - g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) - - // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu (using FindBestPopupWindowPos). - ImVec2 popup_pos, pos = window->DC.CursorPos; - if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) - { - // Menu inside an horizontal menu bar - // Selectable extend their highlight by half ItemSpacing in each direction. - // For ChildMenu, the popup position will be overwritten by the call to FindBestPopupWindowPos() in Begin() - popup_pos = ImVec2(pos.x - window->WindowPadding.x, pos.y - style.FramePadding.y + window->MenuBarHeight()); - window->DC.CursorPos.x += (float) (int) (style.ItemSpacing.x * 0.5f); - PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); - float w = label_size.x; - pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_Menu | ImGuiSelectableFlags_DontClosePopups | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); - PopStyleVar(); - window->DC.CursorPos.x += (float) (int) (style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). - } - else - { - // Menu inside a menu - popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); - float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, (float) (int) (g.FontSize * 1.20f)); // Feedback to next frame - float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); - pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_Menu | ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_DrawFillAvailWidth | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); - if (!enabled) - PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); - RenderTriangle(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), ImGuiDir_Right); - if (!enabled) - PopStyleColor(); - } - - const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id); - if (menuset_is_open) - g.NavWindow = backed_nav_window; - - bool want_open = false, want_close = false; - if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) - { - // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. - bool moving_within_opened_triangle = false; - if (g.HoveredWindow == window && g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentWindow == window && !(window->Flags & ImGuiWindowFlags_MenuBar)) - { - if (ImGuiWindow *next_window = g.OpenPopupStack[g.CurrentPopupStack.Size].Window) - { - ImRect next_window_rect = next_window->Rect(); - ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta; - ImVec2 tb = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR(); - ImVec2 tc = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR(); - float extra = ImClamp(fabsf(ta.x - tb.x) * 0.30f, 5.0f, 30.0f); // add a bit of extra slack. - ta.x += (window->Pos.x < next_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues - tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -100.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale? - tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +100.0f); - moving_within_opened_triangle = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); - // window->DrawList->PushClipRectFullScreen(); window->DrawList->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); window->DrawList->PopClipRect(); // Debug - } - } - - want_close = (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_within_opened_triangle); - want_open = (!menu_is_open && hovered && !moving_within_opened_triangle) || (!menu_is_open && hovered && pressed); - - if (g.NavActivateId == id) - { - want_close = menu_is_open; - want_open = !menu_is_open; - } - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open - { - want_open = true; - NavMoveRequestCancel(); - } - } - else - { - // Menu bar - if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it - { - want_close = true; - want_open = menu_is_open = false; - } - else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others - { - want_open = true; - } - else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open - { - want_open = true; - NavMoveRequestCancel(); - } - } - - if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' - want_close = true; - if (want_close && IsPopupOpen(id)) - ClosePopupToLevel(g.CurrentPopupStack.Size); - - if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.CurrentPopupStack.Size) - { - // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame. - OpenPopup(label); - return false; - } - - menu_is_open |= want_open; - if (want_open) - OpenPopup(label); - - if (menu_is_open) - { - SetNextWindowPos(popup_pos, ImGuiCond_Always); - ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ((window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) ? ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_ChildWindow : ImGuiWindowFlags_ChildMenu); - menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) - } - - return menu_is_open; -} - -void ImGui::EndMenu() -{ - // Nav: When a left move request _within our child menu_ failed, close the menu. - // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. - // However it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) - { - ClosePopupToLevel(g.OpenPopupStack.Size - 1); - NavMoveRequestCancel(); - } - - EndPopup(); -} - -// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. -void ImGui::ColorTooltip(const char *text, const float *col, ImGuiColorEditFlags flags) -{ - ImGuiContext &g = *GImGui; - - int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); - BeginTooltipEx(0, true); - - const char *text_end = text ? FindRenderedTextEnd(text, NULL) : text; - if (text_end > text) - { - TextUnformatted(text, text_end); - Separator(); - } - - ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); - ColorButton("##preview", ImVec4(col[0], col[1], col[2], col[3]), (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); - SameLine(); - if (flags & ImGuiColorEditFlags_NoAlpha) - Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); - else - Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); - EndTooltip(); -} - -static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) -{ - float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; - int r = ImLerp((int) (col_a >> IM_COL32_R_SHIFT) & 0xFF, (int) (col_b >> IM_COL32_R_SHIFT) & 0xFF, t); - int g = ImLerp((int) (col_a >> IM_COL32_G_SHIFT) & 0xFF, (int) (col_b >> IM_COL32_G_SHIFT) & 0xFF, t); - int b = ImLerp((int) (col_a >> IM_COL32_B_SHIFT) & 0xFF, (int) (col_b >> IM_COL32_B_SHIFT) & 0xFF, t); - return IM_COL32(r, g, b, 0xFF); -} - -// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. -// I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether. -void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) - { - ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204, 204, 204, 255), col)); - ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128, 128, 128, 255), col)); - window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags); - - int yi = 0; - for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) - { - float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); - if (y2 <= y1) - continue; - for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) - { - float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); - if (x2 <= x1) - continue; - int rounding_corners_flags_cell = 0; - if (y1 <= p_min.y) - { - if (x1 <= p_min.x) - rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; - if (x2 >= p_max.x) - rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; - } - if (y2 >= p_max.y) - { - if (x1 <= p_min.x) - rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; - if (x2 >= p_max.x) - rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; - } - rounding_corners_flags_cell &= rounding_corners_flags; - window->DrawList->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell); - } - } - } - else - { - window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags); - } -} - -void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) -{ - ImGuiContext &g = *GImGui; - if ((flags & ImGuiColorEditFlags__InputsMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputsMask; - if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask; - if ((flags & ImGuiColorEditFlags__PickerMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask; - IM_ASSERT(ImIsPowerOfTwo((int) (flags & ImGuiColorEditFlags__InputsMask))); // Check only 1 option is selected - IM_ASSERT(ImIsPowerOfTwo((int) (flags & ImGuiColorEditFlags__DataTypeMask))); // Check only 1 option is selected - IM_ASSERT(ImIsPowerOfTwo((int) (flags & ImGuiColorEditFlags__PickerMask))); // Check only 1 option is selected - g.ColorEditOptions = flags; -} - -// A little colored square. Return true when clicked. -// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. -// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. -bool ImGui::ColorButton(const char *desc_id, const ImVec4 &col, ImGuiColorEditFlags flags, ImVec2 size) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiID id = window->GetID(desc_id); - float default_size = GetFrameHeight(); - if (size.x == 0.0f) - size.x = default_size; - if (size.y == 0.0f) - size.y = default_size; - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); - if (!ItemAdd(bb, id)) - return false; - - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); - - if (flags & ImGuiColorEditFlags_NoAlpha) - flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); - - ImVec4 col_without_alpha(col.x, col.y, col.z, 1.0f); - float grid_step = ImMin(size.x, size.y) / 2.99f; - float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); - ImRect bb_inner = bb; - float off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. - bb_inner.Expand(off); - if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col.w < 1.0f) - { - float mid_x = (float) (int) ((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f); - RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight); - window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_without_alpha), rounding, ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft); - } - else - { - // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha - ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col : col_without_alpha; - if (col_source.w < 1.0f) - RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); - else - window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All); - } - RenderNavHighlight(bb, id); - if (g.Style.FrameBorderSize > 0.0f) - RenderFrameBorder(bb.Min, bb.Max, rounding); - else - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border - - // Drag and Drop Source - if (g.ActiveId == id && BeginDragDropSource()) // NB: The ActiveId test is merely an optional micro-optimization - { - if (flags & ImGuiColorEditFlags_NoAlpha) - SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col, sizeof(float) * 3, ImGuiCond_Once); - else - SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col, sizeof(float) * 4, ImGuiCond_Once); - ColorButton(desc_id, col, flags); - SameLine(); - TextUnformatted("Color"); - EndDragDropSource(); - hovered = false; - } - - // Tooltip - if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) - ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); - - return pressed; -} - -bool ImGui::ColorEdit3(const char *label, float col[3], ImGuiColorEditFlags flags) -{ - return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); -} - -void ImGui::ColorEditOptionsPopup(const float *col, ImGuiColorEditFlags flags) -{ - bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__InputsMask); - bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask); - if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) - return; - ImGuiContext &g = *GImGui; - ImGuiColorEditFlags opts = g.ColorEditOptions; - if (allow_opt_inputs) - { - if (RadioButton("RGB", (opts & ImGuiColorEditFlags_RGB) ? 1 : 0)) - opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_RGB; - if (RadioButton("HSV", (opts & ImGuiColorEditFlags_HSV) ? 1 : 0)) - opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HSV; - if (RadioButton("HEX", (opts & ImGuiColorEditFlags_HEX) ? 1 : 0)) - opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HEX; - } - if (allow_opt_datatype) - { - if (allow_opt_inputs) - Separator(); - if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) ? 1 : 0)) - opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8; - if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) ? 1 : 0)) - opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float; - } - - if (allow_opt_inputs || allow_opt_datatype) - Separator(); - if (Button("Copy as..", ImVec2(-1, 0))) - OpenPopup("Copy"); - if (BeginPopup("Copy")) - { - int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); - char buf[64]; - ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); - if (Selectable(buf)) - SetClipboardText(buf); - ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); - if (Selectable(buf)) - SetClipboardText(buf); - if (flags & ImGuiColorEditFlags_NoAlpha) - ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb); - else - ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca); - if (Selectable(buf)) - SetClipboardText(buf); - EndPopup(); - } - - g.ColorEditOptions = opts; - EndPopup(); -} - -static void ColorPickerOptionsPopup(ImGuiColorEditFlags flags, const float *ref_col) -{ - bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask); - bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); - if ((!allow_opt_picker && !allow_opt_alpha_bar) || !ImGui::BeginPopup("context")) - return; - ImGuiContext &g = *GImGui; - if (allow_opt_picker) - { - ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function - ImGui::PushItemWidth(picker_size.x); - for (int picker_type = 0; picker_type < 2; picker_type++) - { - // Draw small/thumbnail version of each picker type (over an invisible button for selection) - if (picker_type > 0) - ImGui::Separator(); - ImGui::PushID(picker_type); - ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha); - if (picker_type == 0) - picker_flags |= ImGuiColorEditFlags_PickerHueBar; - if (picker_type == 1) - picker_flags |= ImGuiColorEditFlags_PickerHueWheel; - ImVec2 backup_pos = ImGui::GetCursorScreenPos(); - if (ImGui::Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup - g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask); - ImGui::SetCursorScreenPos(backup_pos); - ImVec4 dummy_ref_col; - memcpy(&dummy_ref_col.x, ref_col, sizeof(float) * (picker_flags & ImGuiColorEditFlags_NoAlpha ? 3 : 4)); - ImGui::ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags); - ImGui::PopID(); - } - ImGui::PopItemWidth(); - } - if (allow_opt_alpha_bar) - { - if (allow_opt_picker) - ImGui::Separator(); - ImGui::CheckboxFlags("Alpha Bar", (unsigned int *) &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); - } - ImGui::EndPopup(); -} - -// Edit colors components (each component in 0.0f..1.0f range). -// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. -// With typical options: Left-click on colored square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. -bool ImGui::ColorEdit4(const char *label, float col[4], ImGuiColorEditFlags flags) -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext &g = *GImGui; - const ImGuiStyle &style = g.Style; - const float square_sz = GetFrameHeight(); - const float w_extra = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); - const float w_items_all = CalcItemWidth() - w_extra; - const char *label_display_end = FindRenderedTextEnd(label); - - const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; - const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; - const int components = alpha ? 4 : 3; - const ImGuiColorEditFlags flags_untouched = flags; - - BeginGroup(); - PushID(label); - - // If we're not showing any slider there's no point in doing any HSV conversions - if (flags & ImGuiColorEditFlags_NoInputs) - flags = (flags & (~ImGuiColorEditFlags__InputsMask)) | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_NoOptions; - - // Context menu: display and modify options (before defaults are applied) - if (!(flags & ImGuiColorEditFlags_NoOptions)) - ColorEditOptionsPopup(col, flags); - - // Read stored options - if (!(flags & ImGuiColorEditFlags__InputsMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputsMask); - if (!(flags & ImGuiColorEditFlags__DataTypeMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask); - if (!(flags & ImGuiColorEditFlags__PickerMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask); - flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask)); - - // Convert to the formats we need - float f[4] = {col[0], col[1], col[2], alpha ? col[3] : 1.0f}; - if (flags & ImGuiColorEditFlags_HSV) - ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); - int i[4] = {IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3])}; - - bool value_changed = false; - bool value_changed_as_float = false; - - if ((flags & (ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) - { - // RGB/HSV 0..255 Sliders - const float w_item_one = ImMax(1.0f, (float) (int) ((w_items_all - (style.ItemInnerSpacing.x) * (components - 1)) / (float) components)); - const float w_item_last = ImMax(1.0f, (float) (int) (w_items_all - (w_item_one + style.ItemInnerSpacing.x) * (components - 1))); - - const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); - const char *ids[4] = {"##X", "##Y", "##Z", "##W"}; - const char *fmt_table_int[3][4] = - { - {"%3.0f", "%3.0f", "%3.0f", "%3.0f"}, // Short display - {"R:%3.0f", "G:%3.0f", "B:%3.0f", "A:%3.0f"}, // Long display for RGBA - {"H:%3.0f", "S:%3.0f", "V:%3.0f", "A:%3.0f"} // Long display for HSVA - }; - const char *fmt_table_float[3][4] = - { - {"%0.3f", "%0.3f", "%0.3f", "%0.3f"}, // Short display - {"R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f"}, // Long display for RGBA - {"H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f"} // Long display for HSVA - }; - const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_HSV) ? 2 : - 1; - - PushItemWidth(w_item_one); - for (int n = 0; n < components; n++) - { - if (n > 0) - SameLine(0, style.ItemInnerSpacing.x); - if (n + 1 == components) - PushItemWidth(w_item_last); - if (flags & ImGuiColorEditFlags_Float) - value_changed = value_changed_as_float = value_changed | DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); - else - value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); - if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); - } - PopItemWidth(); - PopItemWidth(); - } - else if ((flags & ImGuiColorEditFlags_HEX) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) - { - // RGB Hexadecimal Input - char buf[64]; - if (alpha) - ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255)); - else - ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); - PushItemWidth(w_items_all); - if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) - { - value_changed = true; - char *p = buf; - while (*p == '#' || ImCharIsSpace(*p)) - p++; - i[0] = i[1] = i[2] = i[3] = 0; - if (alpha) - sscanf(p, "%02X%02X%02X%02X", (unsigned int *) &i[0], (unsigned int *) &i[1], (unsigned int *) &i[2], (unsigned int *) &i[3]); // Treat at unsigned (%X is unsigned) - else - sscanf(p, "%02X%02X%02X", (unsigned int *) &i[0], (unsigned int *) &i[1], (unsigned int *) &i[2]); - } - if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); - PopItemWidth(); - } - - ImGuiWindow *picker_active_window = NULL; - if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) - { - if (!(flags & ImGuiColorEditFlags_NoInputs)) - SameLine(0, style.ItemInnerSpacing.x); - - const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); - if (ColorButton("##ColorButton", col_v4, flags)) - { - if (!(flags & ImGuiColorEditFlags_NoPicker)) - { - // Store current color and open a picker - g.ColorPickerRef = col_v4; - OpenPopup("picker"); - SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1, style.ItemSpacing.y)); - } - } - if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); - - if (BeginPopup("picker")) - { - picker_active_window = g.CurrentWindow; - if (label != label_display_end) - { - TextUnformatted(label, label_display_end); - Separator(); - } - ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; - ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; - PushItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? - value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); - PopItemWidth(); - EndPopup(); - } - } - - if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) - { - SameLine(0, style.ItemInnerSpacing.x); - TextUnformatted(label, label_display_end); - } - - // Convert back - if (picker_active_window == NULL) - { - if (!value_changed_as_float) - for (int n = 0; n < 4; n++) - f[n] = i[n] / 255.0f; - if (flags & ImGuiColorEditFlags_HSV) - ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); - if (value_changed) - { - col[0] = f[0]; - col[1] = f[1]; - col[2] = f[2]; - if (alpha) - col[3] = f[3]; - } - } - - PopID(); - EndGroup(); - - // Drag and Drop Target - if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && BeginDragDropTarget()) // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. - { - if (const ImGuiPayload *payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) - { - memcpy((float *) col, payload->Data, sizeof(float) * 3); - value_changed = true; - } - if (const ImGuiPayload *payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) - { - memcpy((float *) col, payload->Data, sizeof(float) * components); - value_changed = true; - } - EndDragDropTarget(); - } - - // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). - if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) - window->DC.LastItemId = g.ActiveId; - - return value_changed; -} - -bool ImGui::ColorPicker3(const char *label, float col[3], ImGuiColorEditFlags flags) -{ - float col4[4] = {col[0], col[1], col[2], 1.0f}; - if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) - return false; - col[0] = col4[0]; - col[1] = col4[1]; - col[2] = col4[2]; - return true; -} - -// 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. -static void RenderArrow(ImDrawList *draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) -{ - switch (direction) - { - case ImGuiDir_Left: - draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); - return; - case ImGuiDir_Right: - draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); - return; - case ImGuiDir_Up: - draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); - return; - case ImGuiDir_Down: - draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); - return; - case ImGuiDir_None: - case ImGuiDir_Count_: - break; // Fix warnings - } -} - -static void RenderArrowsForVerticalBar(ImDrawList *draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w) -{ - RenderArrow(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32_BLACK); - RenderArrow(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32_WHITE); - RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32_BLACK); - RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32_WHITE); -} - -// ColorPicker -// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. -// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) -bool ImGui::ColorPicker4(const char *label, float col[4], ImGuiColorEditFlags flags, const float *ref_col) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - ImDrawList *draw_list = window->DrawList; - - ImGuiStyle &style = g.Style; - ImGuiIO &io = g.IO; - - PushID(label); - BeginGroup(); - - if (!(flags & ImGuiColorEditFlags_NoSidePreview)) - flags |= ImGuiColorEditFlags_NoSmallPreview; - - // Context menu: display and store options. - if (!(flags & ImGuiColorEditFlags_NoOptions)) - ColorPickerOptionsPopup(flags, col); - - // Read stored options - if (!(flags & ImGuiColorEditFlags__PickerMask)) - flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask; - IM_ASSERT(ImIsPowerOfTwo((int) (flags & ImGuiColorEditFlags__PickerMask))); // Check that only 1 is selected - if (!(flags & ImGuiColorEditFlags_NoOptions)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); - - // Setup - int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; - bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); - ImVec2 picker_pos = window->DC.CursorPos; - float square_sz = GetFrameHeight(); - float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars - float sv_picker_size = ImMax(bars_width * 1, CalcItemWidth() - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box - float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; - float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; - float bars_triangles_half_sz = (float) (int) (bars_width * 0.20f); - - float backup_initial_col[4]; - memcpy(backup_initial_col, col, components * sizeof(float)); - - float wheel_thickness = sv_picker_size * 0.08f; - float wheel_r_outer = sv_picker_size * 0.50f; - float wheel_r_inner = wheel_r_outer - wheel_thickness; - ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width) * 0.5f, picker_pos.y + sv_picker_size * 0.5f); - - // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. - float triangle_r = wheel_r_inner - (int) (sv_picker_size * 0.027f); - ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. - ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. - ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. - - float H, S, V; - ColorConvertRGBtoHSV(col[0], col[1], col[2], H, S, V); - - bool value_changed = false, value_changed_h = false, value_changed_sv = false; - - PushItemFlag(ImGuiItemFlags_NoNav, true); - if (flags & ImGuiColorEditFlags_PickerHueWheel) - { - // Hue wheel + SV triangle logic - InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); - if (IsItemActive()) - { - ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; - ImVec2 current_off = g.IO.MousePos - wheel_center; - float initial_dist2 = ImLengthSqr(initial_off); - if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1)) - { - // Interactive with Hue wheel - H = atan2f(current_off.y, current_off.x) / IM_PI * 0.5f; - if (H < 0.0f) - H += 1.0f; - value_changed = value_changed_h = true; - } - float cos_hue_angle = cosf(-H * 2.0f * IM_PI); - float sin_hue_angle = sinf(-H * 2.0f * IM_PI); - if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) - { - // Interacting with SV triangle - ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); - if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) - current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); - float uu, vv, ww; - ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); - V = ImClamp(1.0f - vv, 0.0001f, 1.0f); - S = ImClamp(uu / V, 0.0001f, 1.0f); - value_changed = value_changed_sv = true; - } - } - if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); - } - else if (flags & ImGuiColorEditFlags_PickerHueBar) - { - // SV rectangle logic - InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); - if (IsItemActive()) - { - S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); - V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); - value_changed = value_changed_sv = true; - } - if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); - - // Hue bar logic - SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); - InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); - if (IsItemActive()) - { - H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); - value_changed = value_changed_h = true; - } - } - - // Alpha bar logic - if (alpha_bar) - { - SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); - InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); - if (IsItemActive()) - { - col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); - value_changed = true; - } - } - PopItemFlag(); // ImGuiItemFlags_NoNav - - if (!(flags & ImGuiColorEditFlags_NoSidePreview)) - { - SameLine(0, style.ItemInnerSpacing.x); - BeginGroup(); - } - - if (!(flags & ImGuiColorEditFlags_NoLabel)) - { - const char *label_display_end = FindRenderedTextEnd(label); - if (label != label_display_end) - { - if ((flags & ImGuiColorEditFlags_NoSidePreview)) - SameLine(0, style.ItemInnerSpacing.x); - TextUnformatted(label, label_display_end); - } - } - - if (!(flags & ImGuiColorEditFlags_NoSidePreview)) - { - PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); - ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); - if ((flags & ImGuiColorEditFlags_NoLabel)) - Text("Current"); - ColorButton("##current", col_v4, (flags & (ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2)); - if (ref_col != NULL) - { - Text("Original"); - ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); - if (ColorButton("##original", ref_col_v4, (flags & (ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2))) - { - memcpy(col, ref_col, components * sizeof(float)); - value_changed = true; - } - } - PopItemFlag(); - EndGroup(); - } - - // Convert back color to RGB - if (value_changed_h || value_changed_sv) - ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10 * 1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]); - - // R,G,B and H,S,V slider color editor - if ((flags & ImGuiColorEditFlags_NoInputs) == 0) - { - PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); - ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; - ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; - if (flags & ImGuiColorEditFlags_RGB || (flags & ImGuiColorEditFlags__InputsMask) == 0) - value_changed |= ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_RGB); - if (flags & ImGuiColorEditFlags_HSV || (flags & ImGuiColorEditFlags__InputsMask) == 0) - value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_HSV); - if (flags & ImGuiColorEditFlags_HEX || (flags & ImGuiColorEditFlags__InputsMask) == 0) - value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_HEX); - PopItemWidth(); - } - - // Try to cancel hue wrap (after ColorEdit), if any - if (value_changed) - { - float new_H, new_S, new_V; - ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); - if (new_H <= 0 && H > 0) - { - if (new_V <= 0 && V != new_V) - ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); - else if (new_S <= 0) - ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); - } - } - - ImVec4 hue_color_f(1, 1, 1, 1); - ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); - ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); - ImU32 col32_no_alpha = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 1.0f)); - - const ImU32 hue_colors[6 + 1] = {IM_COL32(255, 0, 0, 255), IM_COL32(255, 255, 0, 255), IM_COL32(0, 255, 0, 255), IM_COL32(0, 255, 255, 255), IM_COL32(0, 0, 255, 255), IM_COL32(255, 0, 255, 255), IM_COL32(255, 0, 0, 255)}; - ImVec2 sv_cursor_pos; - - if (flags & ImGuiColorEditFlags_PickerHueWheel) - { - // Render Hue Wheel - const float aeps = 1.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). - const int segment_per_arc = ImMax(4, (int) wheel_r_outer / 12); - for (int n = 0; n < 6; n++) - { - const float a0 = (n) / 6.0f * 2.0f * IM_PI - aeps; - const float a1 = (n + 1.0f) / 6.0f * 2.0f * IM_PI + aeps; - const int vert_start_idx = draw_list->VtxBuffer.Size; - draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer) * 0.5f, a0, a1, segment_per_arc); - draw_list->PathStroke(IM_COL32_WHITE, false, wheel_thickness); - const int vert_end_idx = draw_list->VtxBuffer.Size; - - // Paint colors over existing vertices - ImVec2 gradient_p0(wheel_center.x + cosf(a0) * wheel_r_inner, wheel_center.y + sinf(a0) * wheel_r_inner); - ImVec2 gradient_p1(wheel_center.x + cosf(a1) * wheel_r_inner, wheel_center.y + sinf(a1) * wheel_r_inner); - ShadeVertsLinearColorGradientKeepAlpha(draw_list->VtxBuffer.Data + vert_start_idx, draw_list->VtxBuffer.Data + vert_end_idx, gradient_p0, gradient_p1, hue_colors[n], hue_colors[n + 1]); - } - - // Render Cursor + preview on Hue Wheel - float cos_hue_angle = cosf(H * 2.0f * IM_PI); - float sin_hue_angle = sinf(H * 2.0f * IM_PI); - ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f); - float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; - int hue_cursor_segments = ImClamp((int) (hue_cursor_rad / 1.4f), 9, 32); - draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); - draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, IM_COL32(128, 128, 128, 255), hue_cursor_segments); - draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, IM_COL32_WHITE, hue_cursor_segments); - - // Render SV triangle (rotated according to hue) - ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); - ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); - ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); - ImVec2 uv_white = GetFontTexUvWhitePixel(); - draw_list->PrimReserve(6, 6); - draw_list->PrimVtx(tra, uv_white, hue_color32); - draw_list->PrimVtx(trb, uv_white, hue_color32); - draw_list->PrimVtx(trc, uv_white, IM_COL32_WHITE); - draw_list->PrimVtx(tra, uv_white, IM_COL32_BLACK_TRANS); - draw_list->PrimVtx(trb, uv_white, IM_COL32_BLACK); - draw_list->PrimVtx(trc, uv_white, IM_COL32_BLACK_TRANS); - draw_list->AddTriangle(tra, trb, trc, IM_COL32(128, 128, 128, 255), 1.5f); - sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); - } - else if (flags & ImGuiColorEditFlags_PickerHueBar) - { - // Render SV Square - draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), IM_COL32_WHITE, hue_color32, hue_color32, IM_COL32_WHITE); - draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32_BLACK, IM_COL32_BLACK); - RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f); - sv_cursor_pos.x = ImClamp((float) (int) (picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much - sv_cursor_pos.y = ImClamp((float) (int) (picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); - - // Render Hue Bar - for (int i = 0; i < 6; ++i) - draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), hue_colors[i], hue_colors[i], hue_colors[i + 1], hue_colors[i + 1]); - float bar0_line_y = (float) (int) (picker_pos.y + H * sv_picker_size + 0.5f); - RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); - RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); - } - - // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) - float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; - draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, col32_no_alpha, 12); - draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, IM_COL32(128, 128, 128, 255), 12); - draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, IM_COL32_WHITE, 12); - - // Render alpha bar - if (alpha_bar) - { - float alpha = ImSaturate(col[3]); - ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); - RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, IM_COL32(0, 0, 0, 0), bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); - draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, col32_no_alpha, col32_no_alpha, col32_no_alpha & ~IM_COL32_A_MASK, col32_no_alpha & ~IM_COL32_A_MASK); - float bar1_line_y = (float) (int) (picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f); - RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); - RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); - } - - EndGroup(); - PopID(); - - return value_changed && memcmp(backup_initial_col, col, components * sizeof(float)); -} - -// Horizontal separating line. -void ImGui::Separator() -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - ImGuiContext &g = *GImGui; - - ImGuiWindowFlags flags = 0; - if ((flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)) == 0) - flags |= (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; - IM_ASSERT(ImIsPowerOfTwo((int) (flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)))); // Check that only 1 option is selected - if (flags & ImGuiSeparatorFlags_Vertical) - { - VerticalSeparator(); - return; - } - - // Horizontal Separator - if (window->DC.ColumnsSet) - PopClipRect(); - - float x1 = window->Pos.x; - float x2 = window->Pos.x + window->Size.x; - if (!window->DC.GroupStack.empty()) - x1 += window->DC.IndentX; - - const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + 1.0f)); - ItemSize(ImVec2(0.0f, 0.0f)); // NB: we don't provide our width so that it doesn't get feed back into AutoFit, we don't provide height to not alter layout. - if (!ItemAdd(bb, 0)) - { - if (window->DC.ColumnsSet) - PushColumnClipRect(); - return; - } - - window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x, bb.Min.y), GetColorU32(ImGuiCol_Separator)); - - if (g.LogEnabled) - LogRenderedText(NULL, IM_NEWLINE "--------------------------------"); - - if (window->DC.ColumnsSet) - { - PushColumnClipRect(); - window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; - } -} - -void ImGui::VerticalSeparator() -{ - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - ImGuiContext &g = *GImGui; - - float y1 = window->DC.CursorPos.y; - float y2 = window->DC.CursorPos.y + window->DC.CurrentLineHeight; - const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + 1.0f, y2)); - ItemSize(ImVec2(bb.GetWidth(), 0.0f)); - if (!ItemAdd(bb, 0)) - return; - - window->DrawList->AddLine(ImVec2(bb.Min.x, bb.Min.y), ImVec2(bb.Min.x, bb.Max.y), GetColorU32(ImGuiCol_Separator)); - if (g.LogEnabled) - LogText(" |"); + bool pressed = (t == 0.0f); + if (!pressed && (flags & ImGuiInputFlags_Repeat) != 0) + { + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(flags, &repeat_delay, &repeat_rate); + pressed = (t > repeat_delay) && GetKeyPressedAmount(key, repeat_delay, repeat_rate) > 0; + if (pressed && (flags & ImGuiInputFlags_RepeatUntilMask_)) + { + // Slightly bias 'key_pressed_time' as DownDuration is an accumulation of DeltaTime which we compare to an absolute time value. + // Ideally we'd replace DownDuration with KeyPressedTime but it would break user's code. + ImGuiContext& g = *GImGui; + double key_pressed_time = g.Time - t + 0.00001f; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChange) && (g.LastKeyModsChangeTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone) && (g.LastKeyModsChangeFromNoneTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilOtherKeyPress) && (g.LastKeyboardKeyPressTime > key_pressed_time)) + pressed = false; + } + } + if (!pressed) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; } -bool ImGui::SplitterBehavior(ImGuiID id, const ImRect &bb, ImGuiAxis axis, float *size1, float *size2, float min_size1, float min_size2, float hover_extend) +bool ImGui::IsKeyReleased(ImGuiKey key) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; + return IsKeyReleased(key, ImGuiKeyOwner_Any); +} - const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; - window->DC.ItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus; - bool item_add = ItemAdd(bb, id); - window->DC.ItemFlags = item_flags_backup; - if (!item_add) - return false; +bool ImGui::IsKeyReleased(ImGuiKey key, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (key_data->DownDurationPrev < 0.0f || key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} - bool hovered, held; - ImRect bb_interact = bb; - bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); - ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); - if (g.ActiveId != id) - SetItemAllowOverlap(); +bool ImGui::IsMouseDown(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // should be same as IsKeyDown(MouseButtonToKey(button), ImGuiKeyOwner_Any), but this allows legacy code hijacking the io.Mousedown[] array. +} - if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id)) - SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); +bool ImGui::IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyDown(MouseButtonToKey(button), owner_id), but this allows legacy code hijacking the io.Mousedown[] array. +} - ImRect bb_render = bb; - if (held) - { - ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min; - float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x; +bool ImGui::IsMouseClicked(ImGuiMouseButton button, bool repeat) +{ + return IsMouseClicked(button, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} - // Minimum pane size - if (mouse_delta < min_size1 - *size1) - mouse_delta = min_size1 - *size1; - if (mouse_delta > *size2 - min_size2) - mouse_delta = *size2 - min_size2; +bool ImGui::IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = g.IO.MouseDownDuration[button]; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsMouseClicked) == 0); // Passing flags not supported by this function! // FIXME: Could support RepeatRate and RepeatUntil flags here. - // Apply resize - *size1 += mouse_delta; - *size2 -= mouse_delta; - bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); - } + const bool repeat = (flags & ImGuiInputFlags_Repeat) != 0; + const bool pressed = (t == 0.0f) || (repeat && t > g.IO.KeyRepeatDelay && CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0); + if (!pressed) + return false; - // Render - const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : - ImGuiCol_Separator); - window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, g.Style.FrameRounding); + if (!TestKeyOwner(MouseButtonToKey(button), owner_id)) + return false; - return held; + return true; } -void ImGui::Spacing() +bool ImGui::IsMouseReleased(ImGuiMouseButton button) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - ItemSize(ImVec2(0, 0)); + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // Should be same as IsKeyReleased(MouseButtonToKey(button), ImGuiKeyOwner_Any) } -void ImGui::Dummy(const ImVec2 &size) +bool ImGui::IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; - - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - ItemSize(bb); - ItemAdd(bb, 0); + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyReleased(MouseButtonToKey(button), owner_id) } -bool ImGui::IsRectVisible(const ImVec2 &size) +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); } -bool ImGui::IsRectVisible(const ImVec2 &rect_min, const ImVec2 &rect_max) +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), owner_id); } -// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) -void ImGui::BeginGroup() +int ImGui::GetMouseClickedCount(ImGuiMouseButton button) { - ImGuiWindow *window = GetCurrentWindow(); - - window->DC.GroupStack.resize(window->DC.GroupStack.Size + 1); - ImGuiGroupData &group_data = window->DC.GroupStack.back(); - group_data.BackupCursorPos = window->DC.CursorPos; - group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; - group_data.BackupIndentX = window->DC.IndentX; - group_data.BackupGroupOffsetX = window->DC.GroupOffsetX; - group_data.BackupCurrentLineHeight = window->DC.CurrentLineHeight; - group_data.BackupCurrentLineTextBaseOffset = window->DC.CurrentLineTextBaseOffset; - group_data.BackupLogLinePosY = window->DC.LogLinePosY; - group_data.BackupActiveIdIsAlive = GImGui->ActiveIdIsAlive; - group_data.AdvanceCursor = true; - - window->DC.GroupOffsetX = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffsetX; - window->DC.IndentX = window->DC.GroupOffsetX; - window->DC.CursorMaxPos = window->DC.CursorPos; - window->DC.CurrentLineHeight = 0.0f; - window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button]; } -void ImGui::EndGroup() +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; - IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(g.CurrentWindow->ClipRect); - ImGuiGroupData &group_data = window->DC.GroupStack.back(); - - ImRect group_bb(group_data.BackupCursorPos, window->DC.CursorMaxPos); - group_bb.Max = ImMax(group_bb.Min, group_bb.Max); - - window->DC.CursorPos = group_data.BackupCursorPos; - window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); - window->DC.CurrentLineHeight = group_data.BackupCurrentLineHeight; - window->DC.CurrentLineTextBaseOffset = group_data.BackupCurrentLineTextBaseOffset; - window->DC.IndentX = group_data.BackupIndentX; - window->DC.GroupOffsetX = group_data.BackupGroupOffsetX; - window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; - - if (group_data.AdvanceCursor) - { - window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrentLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. - ItemSize(group_bb.GetSize(), group_data.BackupCurrentLineTextBaseOffset); - ItemAdd(group_bb, 0); - } - - // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive() will be functional on the entire group. - // It would be be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but if you search for LastItemId you'll notice it is only used in that context. - const bool active_id_within_group = (!group_data.BackupActiveIdIsAlive && g.ActiveIdIsAlive && g.ActiveId && g.ActiveIdWindow->RootWindow == window->RootWindow); - if (active_id_within_group) - window->DC.LastItemId = g.ActiveId; - window->DC.LastItemRect = group_bb; - - window->DC.GroupStack.pop_back(); - - // window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] + // Hit testing, expanded for touch input + if (!rect_clipped.ContainsWithPad(g.IO.MousePos, g.Style.TouchExtraPadding)) + return false; + return true; } -// Gets back to previous line and continue with horizontal layout -// pos_x == 0 : follow right after previous item -// pos_x != 0 : align to specified x position (relative to window/group left) -// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 -// spacing_w >= 0 : enforce spacing amount -void ImGui::SameLine(float pos_x, float spacing_w) +// Return if a mouse click/drag went past the given threshold. Valid to call during the MouseReleased frame. +// [Internal] This doesn't test if the button is pressed +bool ImGui::IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold) { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; +} - ImGuiContext &g = *GImGui; - if (pos_x != 0.0f) - { - if (spacing_w < 0.0f) - spacing_w = 0.0f; - window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + pos_x + spacing_w + window->DC.GroupOffsetX + window->DC.ColumnsOffsetX; - window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; - } - else - { - if (spacing_w < 0.0f) - spacing_w = g.Style.ItemSpacing.x; - window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; - window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; - } - window->DC.CurrentLineHeight = window->DC.PrevLineHeight; - window->DC.CurrentLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; +bool ImGui::IsMouseDragging(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) + return false; + return IsMouseDragPastThreshold(button, lock_threshold); } -void ImGui::NewLine() +ImVec2 ImGui::GetMousePos() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiContext& g = *GImGui; + return g.IO.MousePos; +} - ImGuiContext &g = *GImGui; - const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; - window->DC.LayoutType = ImGuiLayoutType_Vertical; - if (window->DC.CurrentLineHeight > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. - ItemSize(ImVec2(0, 0)); - else - ItemSize(ImVec2(0.0f, g.FontSize)); - window->DC.LayoutType = backup_layout_type; +// This is called TeleportMousePos() and not SetMousePos() to emphasis that setting MousePosPrev will effectively clear mouse delta as well. +// It is expected you only call this if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) is set and supported by backend. +void ImGui::TeleportMousePos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + g.IO.MousePos = g.IO.MousePosPrev = pos; + g.IO.MouseDelta = ImVec2(0.0f, 0.0f); + g.IO.WantSetMousePos = true; + //IMGUI_DEBUG_LOG_IO("TeleportMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); } -void ImGui::NextColumn() +// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! +ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() { - ImGuiWindow *window = GetCurrentWindow(); - if (window->SkipItems || window->DC.ColumnsSet == NULL) - return; + ImGuiContext& g = *GImGui; + if (g.BeginPopupStack.Size > 0) + return g.OpenPopupStack[g.BeginPopupStack.Size - 1].OpenMousePos; + return g.IO.MousePos; +} - ImGuiContext &g = *GImGui; - PopItemWidth(); - PopClipRect(); +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position. +bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +{ + // The assert is only to silence a false-positive in XCode Static Analysis. + // Because GImGui is not dereferenced in every code path, the static analyzer assume that it may be NULL (which it doesn't for other functions). + IM_ASSERT(GImGui != NULL); + const float MOUSE_INVALID = -256000.0f; + ImVec2 p = mouse_pos ? *mouse_pos : GImGui->IO.MousePos; + return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID; +} - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - columns->CellMaxY = ImMax(columns->CellMaxY, window->DC.CursorPos.y); - if (++columns->Current < columns->Count) - { - // Columns 1+ cancel out IndentX - window->DC.ColumnsOffsetX = GetColumnOffset(columns->Current) - window->DC.IndentX + g.Style.ItemSpacing.x; - window->DrawList->ChannelsSetCurrent(columns->Current); - } - else - { - window->DC.ColumnsOffsetX = 0.0f; - window->DrawList->ChannelsSetCurrent(0); - columns->Current = 0; - columns->CellMinY = columns->CellMaxY; - } - window->DC.CursorPos.x = (float) (int) (window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); - window->DC.CursorPos.y = columns->CellMinY; - window->DC.CurrentLineHeight = 0.0f; - window->DC.CurrentLineTextBaseOffset = 0.0f; +// [WILL OBSOLETE] This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. +bool ImGui::IsAnyMouseDown() +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; +} - PushColumnClipRect(); - PushItemWidth(GetColumnWidth() * 0.65f); // FIXME: Move on columns setup +// Return the delta from the initial clicking position while the mouse button is clicked or was just released. +// This is locked and return 0.0f until the mouse moves past a distance threshold at least once. +// NB: This is only valid if IsMousePosValid(). backends in theory should always keep mouse position valid when dragging even outside the client window. +ImVec2 ImGui::GetMouseDragDelta(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + if (g.IO.MouseDown[button] || g.IO.MouseReleased[button]) + if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) + if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MouseClickedPos[button])) + return g.IO.MousePos - g.IO.MouseClickedPos[button]; + return ImVec2(0.0f, 0.0f); } -int ImGui::GetColumnIndex() +void ImGui::ResetMouseDragDelta(ImGuiMouseButton button) { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.ColumnsSet ? window->DC.ColumnsSet->Current : 0; + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr + g.IO.MouseClickedPos[button] = g.IO.MousePos; } -int ImGui::GetColumnsCount() +// Get desired mouse cursor shape. +// Important: this is meant to be used by a platform backend, it is reset in ImGui::NewFrame(), +// updated during the frame, and locked in EndFrame()/Render(). +// If you use software rendering by setting io.MouseDrawCursor then Dear ImGui will render those for you +ImGuiMouseCursor ImGui::GetMouseCursor() { - ImGuiWindow *window = GetCurrentWindowRead(); - return window->DC.ColumnsSet ? window->DC.ColumnsSet->Count : 1; + ImGuiContext& g = *GImGui; + return g.MouseCursor; } -static float OffsetNormToPixels(const ImGuiColumnsSet *columns, float offset_norm) +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) { - return offset_norm * (columns->MaxX - columns->MinX); + ImGuiContext& g = *GImGui; + g.MouseCursor = cursor_type; } -static float PixelsToOffsetNorm(const ImGuiColumnsSet *columns, float offset) +static void UpdateAliasKey(ImGuiKey key, bool v, float analog_value) { - return offset / (columns->MaxX - columns->MinX); + IM_ASSERT(ImGui::IsAliasKey(key)); + ImGuiKeyData* key_data = ImGui::GetKeyData(key); + key_data->Down = v; + key_data->AnalogValue = analog_value; } -static inline float GetColumnsRectHalfWidth() +// [Internal] Do not use directly +static ImGuiKeyChord GetMergedModsFromKeys() { - return 4.0f; + ImGuiKeyChord mods = 0; + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) { mods |= ImGuiMod_Ctrl; } + if (ImGui::IsKeyDown(ImGuiMod_Shift)) { mods |= ImGuiMod_Shift; } + if (ImGui::IsKeyDown(ImGuiMod_Alt)) { mods |= ImGuiMod_Alt; } + if (ImGui::IsKeyDown(ImGuiMod_Super)) { mods |= ImGuiMod_Super; } + return mods; } -static float GetDraggedColumnOffset(ImGuiColumnsSet *columns, int column_index) +static void ImGui::UpdateKeyboardInputs() { - // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing - // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - IM_ASSERT(column_index > 0); // We cannot drag column 0. If you get this assert you may have a conflict between the ID of your columns and another widgets. - IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + io.ClearInputKeys(); + + // Import legacy keys or verify they are not used +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + if (io.BackendUsingLegacyKeyArrays == 0) + { + // Backend used new io.AddKeyEvent() API: Good! Verify that old arrays are never written to externally. + for (int n = 0; n < ImGuiKey_LegacyNativeKey_END; n++) + IM_ASSERT((io.KeysDown[n] == false || IsKeyDown((ImGuiKey)n)) && "Backend needs to either only use io.AddKeyEvent(), either only fill legacy io.KeysDown[] + io.KeyMap[]. Not both!"); + } + else + { + if (g.FrameCount == 0) + for (int n = ImGuiKey_LegacyNativeKey_BEGIN; n < ImGuiKey_LegacyNativeKey_END; n++) + IM_ASSERT(g.IO.KeyMap[n] == -1 && "Backend is not allowed to write to io.KeyMap[0..511]!"); + + // Build reverse KeyMap (Named -> Legacy) + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_NamedKey_END; n++) + if (io.KeyMap[n] != -1) + { + IM_ASSERT(IsLegacyKey((ImGuiKey)io.KeyMap[n])); + io.KeyMap[io.KeyMap[n]] = n; + } + + // Import legacy keys into new ones + for (int n = ImGuiKey_LegacyNativeKey_BEGIN; n < ImGuiKey_LegacyNativeKey_END; n++) + if (io.KeysDown[n] || io.BackendUsingLegacyKeyArrays == 1) + { + const ImGuiKey key = (ImGuiKey)(io.KeyMap[n] != -1 ? io.KeyMap[n] : n); + IM_ASSERT(io.KeyMap[n] == -1 || IsNamedKey(key)); + io.KeysData[key].Down = io.KeysDown[n]; + if (key != n) + io.KeysDown[key] = io.KeysDown[n]; // Allow legacy code using io.KeysDown[GetKeyIndex()] with old backends + io.BackendUsingLegacyKeyArrays = 1; + } + if (io.BackendUsingLegacyKeyArrays == 1) + { + GetKeyData(ImGuiMod_Ctrl)->Down = io.KeyCtrl; + GetKeyData(ImGuiMod_Shift)->Down = io.KeyShift; + GetKeyData(ImGuiMod_Alt)->Down = io.KeyAlt; + GetKeyData(ImGuiMod_Super)->Down = io.KeySuper; + } + } +#endif + + // Import legacy ImGuiNavInput_ io inputs and convert to gamepad keys +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + if (io.BackendUsingLegacyNavInputArray && nav_gamepad_active) + { + #define MAP_LEGACY_NAV_INPUT_TO_KEY1(_KEY, _NAV1) do { io.KeysData[_KEY].Down = (io.NavInputs[_NAV1] > 0.0f); io.KeysData[_KEY].AnalogValue = io.NavInputs[_NAV1]; } while (0) + #define MAP_LEGACY_NAV_INPUT_TO_KEY2(_KEY, _NAV1, _NAV2) do { io.KeysData[_KEY].Down = (io.NavInputs[_NAV1] > 0.0f) || (io.NavInputs[_NAV2] > 0.0f); io.KeysData[_KEY].AnalogValue = ImMax(io.NavInputs[_NAV1], io.NavInputs[_NAV2]); } while (0) + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadFaceDown, ImGuiNavInput_Activate); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadFaceRight, ImGuiNavInput_Cancel); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadFaceLeft, ImGuiNavInput_Menu); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadFaceUp, ImGuiNavInput_Input); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadDpadLeft, ImGuiNavInput_DpadLeft); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadDpadRight, ImGuiNavInput_DpadRight); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadDpadUp, ImGuiNavInput_DpadUp); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadDpadDown, ImGuiNavInput_DpadDown); + MAP_LEGACY_NAV_INPUT_TO_KEY2(ImGuiKey_GamepadL1, ImGuiNavInput_FocusPrev, ImGuiNavInput_TweakSlow); + MAP_LEGACY_NAV_INPUT_TO_KEY2(ImGuiKey_GamepadR1, ImGuiNavInput_FocusNext, ImGuiNavInput_TweakFast); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadLStickLeft, ImGuiNavInput_LStickLeft); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadLStickRight, ImGuiNavInput_LStickRight); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadLStickUp, ImGuiNavInput_LStickUp); + MAP_LEGACY_NAV_INPUT_TO_KEY1(ImGuiKey_GamepadLStickDown, ImGuiNavInput_LStickDown); + #undef NAV_MAP_KEY + } +#endif + + // Update aliases + for (int n = 0; n < ImGuiMouseButton_COUNT; n++) + UpdateAliasKey(MouseButtonToKey(n), io.MouseDown[n], io.MouseDown[n] ? 1.0f : 0.0f); + UpdateAliasKey(ImGuiKey_MouseWheelX, io.MouseWheelH != 0.0f, io.MouseWheelH); + UpdateAliasKey(ImGuiKey_MouseWheelY, io.MouseWheel != 0.0f, io.MouseWheel); + + // Synchronize io.KeyMods and io.KeyCtrl/io.KeyShift/etc. values. + // - New backends (1.87+): send io.AddKeyEvent(ImGuiMod_XXX) -> -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // - Legacy backends: set io.KeyXXX bools -> (above) set key array from io.KeyXXX -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // So with legacy backends the 4 values will do a unnecessary back-and-forth but it makes the code simpler and future facing. + const ImGuiKeyChord prev_key_mods = io.KeyMods; + io.KeyMods = GetMergedModsFromKeys(); + io.KeyCtrl = (io.KeyMods & ImGuiMod_Ctrl) != 0; + io.KeyShift = (io.KeyMods & ImGuiMod_Shift) != 0; + io.KeyAlt = (io.KeyMods & ImGuiMod_Alt) != 0; + io.KeySuper = (io.KeyMods & ImGuiMod_Super) != 0; + if (prev_key_mods != io.KeyMods) + g.LastKeyModsChangeTime = g.Time; + if (prev_key_mods != io.KeyMods && prev_key_mods == 0) + g.LastKeyModsChangeFromNoneTime = g.Time; + + // Clear gamepad data if disabled + if ((io.BackendFlags & ImGuiBackendFlags_HasGamepad) == 0) + for (int i = ImGuiKey_Gamepad_BEGIN; i < ImGuiKey_Gamepad_END; i++) + { + io.KeysData[i - ImGuiKey_KeysData_OFFSET].Down = false; + io.KeysData[i - ImGuiKey_KeysData_OFFSET].AnalogValue = 0.0f; + } + + // Update keys + for (int i = 0; i < ImGuiKey_KeysData_SIZE; i++) + { + ImGuiKeyData* key_data = &io.KeysData[i]; + key_data->DownDurationPrev = key_data->DownDuration; + key_data->DownDuration = key_data->Down ? (key_data->DownDuration < 0.0f ? 0.0f : key_data->DownDuration + io.DeltaTime) : -1.0f; + if (key_data->DownDuration == 0.0f) + { + ImGuiKey key = (ImGuiKey)(ImGuiKey_KeysData_OFFSET + i); + if (IsKeyboardKey(key)) + g.LastKeyboardKeyPressTime = g.Time; + else if (key == ImGuiKey_ReservedForModCtrl || key == ImGuiKey_ReservedForModShift || key == ImGuiKey_ReservedForModAlt || key == ImGuiKey_ReservedForModSuper) + g.LastKeyboardKeyPressTime = g.Time; + } + } - float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + GetColumnsRectHalfWidth() - window->Pos.x; - x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); - if ((columns->Flags & ImGuiColumnsFlags_NoPreserveWidths)) - x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); + // Update keys/input owner (named keys only): one entry per key + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &io.KeysData[key - ImGuiKey_KeysData_OFFSET]; + ImGuiKeyOwnerData* owner_data = &g.KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; + owner_data->OwnerCurr = owner_data->OwnerNext; + if (!key_data->Down) // Important: ownership is released on the frame after a release. Ensure a 'MouseDown -> CloseWindow -> MouseUp' chain doesn't lead to someone else seeing the MouseUp. + owner_data->OwnerNext = ImGuiKeyOwner_NoOwner; + owner_data->LockThisFrame = owner_data->LockUntilRelease = owner_data->LockUntilRelease && key_data->Down; // Clear LockUntilRelease when key is not Down anymore + } - return x; + // Update key routing (for e.g. shortcuts) + UpdateKeyRoutingTable(&g.KeysRoutingTable); } -float ImGui::GetColumnOffset(int column_index) +static void ImGui::UpdateMouseInputs() { - ImGuiWindow *window = GetCurrentWindowRead(); - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Mouse Wheel swapping flag + // As a standard behavior holding SHIFT while using Vertical Mouse Wheel triggers Horizontal scroll instead + // - We avoid doing it on OSX as it the OS input layer handles this already. + // - FIXME: However this means when running on OSX over Emscripten, Shift+WheelY will incur two swapping (1 in OS, 1 here), canceling the feature. + // - FIXME: When we can distinguish e.g. touchpad scroll events from mouse ones, we'll set this accordingly based on input source. + io.MouseWheelRequestAxisSwap = io.KeyShift && !io.ConfigMacOSXBehaviors; - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); + // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) + if (IsMousePosValid(&io.MousePos)) + io.MousePos = g.MouseLastValidPos = ImFloor(io.MousePos); - /* - if (g.ActiveId) - { - ImGuiContext& g = *GImGui; - const ImGuiID column_id = columns->ColumnsSetId + ImGuiID(column_index); - if (g.ActiveId == column_id) - return GetDraggedColumnOffset(columns, column_index); - } - */ + // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta + if (IsMousePosValid(&io.MousePos) && IsMousePosValid(&io.MousePosPrev)) + io.MouseDelta = io.MousePos - io.MousePosPrev; + else + io.MouseDelta = ImVec2(0.0f, 0.0f); - const float t = columns->Columns[column_index].OffsetNorm; - const float x_offset = ImLerp(columns->MinX, columns->MaxX, t); - return x_offset; + // Update stationary timer. + // FIXME: May need to rework again to have some tolerance for occasional small movement, while being functional on high-framerates. + const float mouse_stationary_threshold = (io.MouseSource == ImGuiMouseSource_Mouse) ? 2.0f : 3.0f; // Slightly higher threshold for ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen, may need rework. + const bool mouse_stationary = (ImLengthSqr(io.MouseDelta) <= mouse_stationary_threshold * mouse_stationary_threshold); + g.MouseStationaryTimer = mouse_stationary ? (g.MouseStationaryTimer + io.DeltaTime) : 0.0f; + //IMGUI_DEBUG_LOG("%.4f\n", g.MouseStationaryTimer); + + // If mouse moved we re-enable mouse hovering in case it was disabled by gamepad/keyboard. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true. + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) + g.NavDisableMouseHover = false; + + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f; + io.MouseClickedCount[i] = 0; // Will be filled below + io.MouseReleased[i] = !io.MouseDown[i] && io.MouseDownDuration[i] >= 0.0f; + io.MouseDownDurationPrev[i] = io.MouseDownDuration[i]; + io.MouseDownDuration[i] = io.MouseDown[i] ? (io.MouseDownDuration[i] < 0.0f ? 0.0f : io.MouseDownDuration[i] + io.DeltaTime) : -1.0f; + if (io.MouseClicked[i]) + { + bool is_repeated_click = false; + if ((float)(g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime) + { + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + if (ImLengthSqr(delta_from_click_pos) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist) + is_repeated_click = true; + } + if (is_repeated_click) + io.MouseClickedLastCount[i]++; + else + io.MouseClickedLastCount[i] = 1; + io.MouseClickedTime[i] = g.Time; + io.MouseClickedPos[i] = io.MousePos; + io.MouseClickedCount[i] = io.MouseClickedLastCount[i]; + io.MouseDragMaxDistanceSqr[i] = 0.0f; + } + else if (io.MouseDown[i]) + { + // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold + float delta_sqr_click_pos = IsMousePosValid(&io.MousePos) ? ImLengthSqr(io.MousePos - io.MouseClickedPos[i]) : 0.0f; + io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], delta_sqr_click_pos); + } + + // We provide io.MouseDoubleClicked[] as a legacy service + io.MouseDoubleClicked[i] = (io.MouseClickedCount[i] == 2); + + // Clicking any mouse button reactivate mouse hovering which may have been deactivated by gamepad/keyboard navigation + if (io.MouseClicked[i]) + g.NavDisableMouseHover = false; + } } -static float GetColumnWidthEx(ImGuiColumnsSet *columns, int column_index, bool before_resize = false) +static void LockWheelingWindow(ImGuiWindow* window, float wheel_amount) { - if (column_index < 0) - column_index = columns->Current; + ImGuiContext& g = *GImGui; + if (window) + g.WheelingWindowReleaseTimer = ImMin(g.WheelingWindowReleaseTimer + ImAbs(wheel_amount) * WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER, WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER); + else + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindow == window) + return; + IMGUI_DEBUG_LOG_IO("[io] LockWheelingWindow() \"%s\"\n", window ? window->Name : "NULL"); + g.WheelingWindow = window; + g.WheelingWindowRefMousePos = g.IO.MousePos; + if (window == NULL) + { + g.WheelingWindowStartFrame = -1; + g.WheelingAxisAvg = ImVec2(0.0f, 0.0f); + } +} - float offset_norm; - if (before_resize) - offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; - else - offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; - return OffsetNormToPixels(columns, offset_norm); +static ImGuiWindow* FindBestWheelingWindow(const ImVec2& wheel) +{ + // For each axis, find window in the hierarchy that may want to use scrolling + ImGuiContext& g = *GImGui; + ImGuiWindow* windows[2] = { NULL, NULL }; + for (int axis = 0; axis < 2; axis++) + if (wheel[axis] != 0.0f) + for (ImGuiWindow* window = windows[axis] = g.HoveredWindow; window->Flags & ImGuiWindowFlags_ChildWindow; window = windows[axis] = window->ParentWindow) + { + // Bubble up into parent window if: + // - a child window doesn't allow any scrolling. + // - a child window has the ImGuiWindowFlags_NoScrollWithMouse flag. + //// - a child window doesn't need scrolling because it is already at the edge for the direction we are going in (FIXME-WIP) + const bool has_scrolling = (window->ScrollMax[axis] != 0.0f); + const bool inputs_disabled = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs); + //const bool scrolling_past_limits = (wheel_v < 0.0f) ? (window->Scroll[axis] <= 0.0f) : (window->Scroll[axis] >= window->ScrollMax[axis]); + if (has_scrolling && !inputs_disabled) // && !scrolling_past_limits) + break; // select this window + } + if (windows[0] == NULL && windows[1] == NULL) + return NULL; + + // If there's only one window or only one axis then there's no ambiguity + if (windows[0] == windows[1] || windows[0] == NULL || windows[1] == NULL) + return windows[1] ? windows[1] : windows[0]; + + // If candidate are different windows we need to decide which one to prioritize + // - First frame: only find a winner if one axis is zero. + // - Subsequent frames: only find a winner when one is more than the other. + if (g.WheelingWindowStartFrame == -1) + g.WheelingWindowStartFrame = g.FrameCount; + if ((g.WheelingWindowStartFrame == g.FrameCount && wheel.x != 0.0f && wheel.y != 0.0f) || (g.WheelingAxisAvg.x == g.WheelingAxisAvg.y)) + { + g.WheelingWindowWheelRemainder = wheel; + return NULL; + } + return (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? windows[0] : windows[1]; } -float ImGui::GetColumnWidth(int column_index) +// Called by NewFrame() +void ImGui::UpdateMouseWheel() { - ImGuiWindow *window = GetCurrentWindowRead(); - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + // Reset the locked window if we move the mouse or after the timer elapses. + // FIXME: Ideally we could refactor to have one timer for "changing window w/ same axis" and a shorter timer for "changing window or axis w/ other axis" (#3795) + ImGuiContext& g = *GImGui; + if (g.WheelingWindow != NULL) + { + g.WheelingWindowReleaseTimer -= g.IO.DeltaTime; + if (IsMousePosValid() && ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindowReleaseTimer <= 0.0f) + LockWheelingWindow(NULL, 0.0f); + } + + ImVec2 wheel; + wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheelH : 0.0f; + wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheel : 0.0f; - if (column_index < 0) - column_index = columns->Current; - return OffsetNormToPixels(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); + //IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y); + ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; + if (!mouse_window || mouse_window->Collapsed) + return; + + // Zoom / Scale window + // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. + if (wheel.y != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) + { + LockWheelingWindow(mouse_window, wheel.y); + ImGuiWindow* window = mouse_window; + const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); + const float scale = new_font_scale / window->FontWindowScale; + window->FontWindowScale = new_font_scale; + if (window == window->RootWindow) + { + const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; + SetWindowPos(window, window->Pos + offset, 0); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + } + return; + } + if (g.IO.KeyCtrl) + return; + + // Mouse wheel scrolling + // Read about io.MouseWheelRequestAxisSwap and its issue on Mac+Emscripten in UpdateMouseInputs() + if (g.IO.MouseWheelRequestAxisSwap) + wheel = ImVec2(wheel.y, 0.0f); + + // Maintain a rough average of moving magnitude on both axises + // FIXME: should by based on wall clock time rather than frame-counter + g.WheelingAxisAvg.x = ImExponentialMovingAverage(g.WheelingAxisAvg.x, ImAbs(wheel.x), 30); + g.WheelingAxisAvg.y = ImExponentialMovingAverage(g.WheelingAxisAvg.y, ImAbs(wheel.y), 30); + + // In the rare situation where FindBestWheelingWindow() had to defer first frame of wheeling due to ambiguous main axis, reinject it now. + wheel += g.WheelingWindowWheelRemainder; + g.WheelingWindowWheelRemainder = ImVec2(0.0f, 0.0f); + if (wheel.x == 0.0f && wheel.y == 0.0f) + return; + + // Mouse wheel scrolling: find target and apply + // - don't renew lock if axis doesn't apply on the window. + // - select a main axis when both axises are being moved. + if (ImGuiWindow* window = (g.WheelingWindow ? g.WheelingWindow : FindBestWheelingWindow(wheel))) + if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) + { + bool do_scroll[2] = { wheel.x != 0.0f && window->ScrollMax.x != 0.0f, wheel.y != 0.0f && window->ScrollMax.y != 0.0f }; + if (do_scroll[ImGuiAxis_X] && do_scroll[ImGuiAxis_Y]) + do_scroll[(g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? ImGuiAxis_Y : ImGuiAxis_X] = false; + if (do_scroll[ImGuiAxis_X]) + { + LockWheelingWindow(window, wheel.x); + float max_step = window->InnerRect.GetWidth() * 0.67f; + float scroll_step = ImTrunc(ImMin(2 * window->CalcFontSize(), max_step)); + SetScrollX(window, window->Scroll.x - wheel.x * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + if (do_scroll[ImGuiAxis_Y]) + { + LockWheelingWindow(window, wheel.y); + float max_step = window->InnerRect.GetHeight() * 0.67f; + float scroll_step = ImTrunc(ImMin(5 * window->CalcFontSize(), max_step)); + SetScrollY(window, window->Scroll.y - wheel.y * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + } +} + +void ImGui::SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureKeyboardNextFrame = want_capture_keyboard ? 1 : 0; +} + +void ImGui::SetNextFrameWantCaptureMouse(bool want_capture_mouse) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureMouseNextFrame = want_capture_mouse ? 1 : 0; +} + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +static const char* GetInputSourceName(ImGuiInputSource source) +{ + const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad" }; + IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT); + return input_source_names[source]; +} +static const char* GetMouseSourceName(ImGuiMouseSource source) +{ + const char* mouse_source_names[] = { "Mouse", "TouchScreen", "Pen" }; + IM_ASSERT(IM_ARRAYSIZE(mouse_source_names) == ImGuiMouseSource_COUNT && source >= 0 && source < ImGuiMouseSource_COUNT); + return mouse_source_names[source]; +} +static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e) +{ + ImGuiContext& g = *GImGui; + if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (%.1f, %.1f) (%s)\n", prefix, e->MousePos.PosX, e->MousePos.PosY, GetMouseSourceName(e->MousePos.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseButton %d %s (%s)\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up", GetMouseSourceName(e->MouseButton.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseWheel (%.3f, %.3f) (%s)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY, GetMouseSourceName(e->MouseWheel.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_Key) { IMGUI_DEBUG_LOG_IO("[io] %s: Key \"%s\" %s\n", prefix, ImGui::GetKeyName(e->Key.Key), e->Key.Down ? "Down" : "Up"); return; } + if (e->Type == ImGuiInputEventType_Text) { IMGUI_DEBUG_LOG_IO("[io] %s: Text: %c (U+%08X)\n", prefix, e->Text.Char, e->Text.Char); return; } + if (e->Type == ImGuiInputEventType_Focus) { IMGUI_DEBUG_LOG_IO("[io] %s: AppFocused %d\n", prefix, e->AppFocused.Focused); return; } } +#endif -void ImGui::SetColumnOffset(int column_index, float offset) +// Process input queue +// We always call this with the value of 'bool g.IO.ConfigInputTrickleEventQueue'. +// - trickle_fast_inputs = false : process all events, turn into flattened input state (e.g. successive down/up/down/up will be lost) +// - trickle_fast_inputs = true : process as many events as possible (successive down/up/down/up will be trickled over several frames so nothing is lost) (new feature in 1.87) +void ImGui::UpdateInputEvents(bool trickle_fast_inputs) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); + // Only trickle chars<>key when working with InputText() + // FIXME: InputText() could parse event trail? + // FIXME: Could specialize chars<>keys trickling rules for control keys (those not typically associated to characters) + const bool trickle_interleaved_nonchar_keys_and_text = (trickle_fast_inputs && g.WantTextInputNextFrame == 1); - const bool preserve_width = !(columns->Flags & ImGuiColumnsFlags_NoPreserveWidths) && (column_index < columns->Count - 1); - const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; + bool mouse_moved = false, mouse_wheeled = false, key_changed = false, key_changed_nonchar = false, text_inputted = false; + int mouse_button_changed = 0x00; + ImBitArray key_changed_mask; - if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow)) - offset = ImMin(offset, columns->MaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); - columns->Columns[column_index].OffsetNorm = PixelsToOffsetNorm(columns, offset - columns->MinX); + int event_n = 0; + for (; event_n < g.InputEventsQueue.Size; event_n++) + { + ImGuiInputEvent* e = &g.InputEventsQueue[event_n]; + if (e->Type == ImGuiInputEventType_MousePos) + { + if (g.IO.WantSetMousePos) + continue; + // Trickling Rule: Stop processing queued events if we already handled a mouse button change + ImVec2 event_pos(e->MousePos.PosX, e->MousePos.PosY); + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_wheeled || key_changed || text_inputted)) + break; + io.MousePos = event_pos; + io.MouseSource = e->MousePos.MouseSource; + mouse_moved = true; + } + else if (e->Type == ImGuiInputEventType_MouseButton) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + const ImGuiMouseButton button = e->MouseButton.Button; + IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); + if (trickle_fast_inputs && ((mouse_button_changed & (1 << button)) || mouse_wheeled)) + break; + if (trickle_fast_inputs && e->MouseButton.MouseSource == ImGuiMouseSource_TouchScreen && mouse_moved) // #2702: TouchScreen have no initial hover. + break; + io.MouseDown[button] = e->MouseButton.Down; + io.MouseSource = e->MouseButton.MouseSource; + mouse_button_changed |= (1 << button); + } + else if (e->Type == ImGuiInputEventType_MouseWheel) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the event + if (trickle_fast_inputs && (mouse_moved || mouse_button_changed != 0)) + break; + io.MouseWheelH += e->MouseWheel.WheelX; + io.MouseWheel += e->MouseWheel.WheelY; + io.MouseSource = e->MouseWheel.MouseSource; + mouse_wheeled = true; + } + else if (e->Type == ImGuiInputEventType_Key) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + ImGuiKey key = e->Key.Key; + IM_ASSERT(key != ImGuiKey_None); + ImGuiKeyData* key_data = GetKeyData(key); + const int key_data_index = (int)(key_data - g.IO.KeysData); + if (trickle_fast_inputs && key_data->Down != e->Key.Down && (key_changed_mask.TestBit(key_data_index) || mouse_button_changed != 0)) + break; + + const bool key_is_potentially_for_char_input = IsKeyChordPotentiallyCharInput(GetMergedModsFromKeys() | key); + if (trickle_interleaved_nonchar_keys_and_text && (text_inputted && !key_is_potentially_for_char_input)) + break; + + key_data->Down = e->Key.Down; + key_data->AnalogValue = e->Key.AnalogValue; + key_changed = true; + key_changed_mask.SetBit(key_data_index); + if (trickle_interleaved_nonchar_keys_and_text && !key_is_potentially_for_char_input) + key_changed_nonchar = true; + + // Allow legacy code using io.KeysDown[GetKeyIndex()] with new backends +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + io.KeysDown[key_data_index] = key_data->Down; + if (io.KeyMap[key_data_index] != -1) + io.KeysDown[io.KeyMap[key_data_index]] = key_data->Down; +#endif + } + else if (e->Type == ImGuiInputEventType_Text) + { + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + // Trickling Rule: Stop processing queued events if keys/mouse have been interacted with + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_moved || mouse_wheeled)) + break; + if (trickle_interleaved_nonchar_keys_and_text && key_changed_nonchar) + break; + unsigned int c = e->Text.Char; + io.InputQueueCharacters.push_back(c <= IM_UNICODE_CODEPOINT_MAX ? (ImWchar)c : IM_UNICODE_CODEPOINT_INVALID); + if (trickle_interleaved_nonchar_keys_and_text) + text_inputted = true; + } + else if (e->Type == ImGuiInputEventType_Focus) + { + // We intentionally overwrite this and process in NewFrame(), in order to give a chance + // to multi-viewports backends to queue AddFocusEvent(false) + AddFocusEvent(true) in same frame. + const bool focus_lost = !e->AppFocused.Focused; + io.AppFocusLost = focus_lost; + } + else + { + IM_ASSERT(0 && "Unknown event!"); + } + } - if (preserve_width) - SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); -} + // Record trail (for domain-specific applications wanting to access a precise trail) + //if (event_n != 0) IMGUI_DEBUG_LOG_IO("Processed: %d / Remaining: %d\n", event_n, g.InputEventsQueue.Size - event_n); + for (int n = 0; n < event_n; n++) + g.InputEventsTrail.push_back(g.InputEventsQueue[n]); -void ImGui::SetColumnWidth(int column_index, float width) -{ - ImGuiWindow *window = GetCurrentWindowRead(); - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (event_n != 0 && (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO)) + for (int n = 0; n < g.InputEventsQueue.Size; n++) + DebugPrintInputEvent(n < event_n ? "Processed" : "Remaining", &g.InputEventsQueue[n]); +#endif - if (column_index < 0) - column_index = columns->Current; - SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); + // Remaining events will be processed on the next frame + if (event_n == g.InputEventsQueue.Size) + g.InputEventsQueue.resize(0); + else + g.InputEventsQueue.erase(g.InputEventsQueue.Data, g.InputEventsQueue.Data + event_n); + + // Clear buttons state when focus is lost + // - this is useful so e.g. releasing Alt after focus loss on Alt-Tab doesn't trigger the Alt menu toggle. + // - we clear in EndFrame() and not now in order allow application/user code polling this flag + // (e.g. custom backend may want to clear additional data, custom widgets may want to react with a "canceling" event). + if (g.IO.AppFocusLost) + { + g.IO.ClearInputKeys(); + g.IO.ClearInputMouse(); + } } -void ImGui::PushColumnClipRect(int column_index) +ImGuiID ImGui::GetKeyOwner(ImGuiKey key) { - ImGuiWindow *window = GetCurrentWindowRead(); - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - if (column_index < 0) - column_index = columns->Current; + if (!IsNamedKeyOrMod(key)) + return ImGuiKeyOwner_NoOwner; - PushClipRect(columns->Columns[column_index].ClipRect.Min, columns->Columns[column_index].ClipRect.Max, false); + ImGuiContext& g = *GImGui; + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + ImGuiID owner_id = owner_data->OwnerCurr; + + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return ImGuiKeyOwner_NoOwner; + + return owner_id; } -static ImGuiColumnsSet *FindOrAddColumnsSet(ImGuiWindow *window, ImGuiID id) +// TestKeyOwner(..., ID) : (owner == None || owner == ID) +// TestKeyOwner(..., None) : (owner == None) +// TestKeyOwner(..., Any) : no owner test +// All paths are also testing for key not being locked, for the rare cases that key have been locked with using ImGuiInputFlags_LockXXX flags. +bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id) { - for (int n = 0; n < window->ColumnsStorage.Size; n++) - if (window->ColumnsStorage[n].ID == id) - return &window->ColumnsStorage[n]; + if (!IsNamedKeyOrMod(key)) + return true; + + ImGuiContext& g = *GImGui; + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_id == ImGuiKeyOwner_Any) + return (owner_data->LockThisFrame == false); - window->ColumnsStorage.push_back(ImGuiColumnsSet()); - ImGuiColumnsSet *columns = &window->ColumnsStorage.back(); - columns->ID = id; - return columns; -} - -void ImGui::BeginColumns(const char *str_id, int columns_count, ImGuiColumnsFlags flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - - IM_ASSERT(columns_count > 1); - IM_ASSERT(window->DC.ColumnsSet == NULL); // Nested columns are currently not supported - - // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. - // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. - PushID(0x11223347 + (str_id ? 0 : columns_count)); - ImGuiID id = window->GetID(str_id ? str_id : "columns"); - PopID(); - - // Acquire storage for the columns set - ImGuiColumnsSet *columns = FindOrAddColumnsSet(window, id); - IM_ASSERT(columns->ID == id); - columns->Current = 0; - columns->Count = columns_count; - columns->Flags = flags; - window->DC.ColumnsSet = columns; - - // Set state for first column - const float content_region_width = (window->SizeContentsExplicit.x != 0.0f) ? (window->SizeContentsExplicit.x) : (window->Size.x - window->ScrollbarSizes.x); - columns->MinX = window->DC.IndentX - g.Style.ItemSpacing.x; // Lock our horizontal range - // column->MaxX = content_region_width - window->Scroll.x - ((window->Flags & ImGuiWindowFlags_NoScrollbar) ? 0 : g.Style.ScrollbarSize);// - window->WindowPadding().x; - columns->MaxX = content_region_width - window->Scroll.x; - columns->StartPosY = window->DC.CursorPos.y; - columns->StartMaxPosX = window->DC.CursorMaxPos.x; - columns->CellMinY = columns->CellMaxY = window->DC.CursorPos.y; - window->DC.ColumnsOffsetX = 0.0f; - window->DC.CursorPos.x = (float) (int) (window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); - - // Clear data if columns count changed - if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) - columns->Columns.resize(0); - - // Initialize defaults - columns->IsFirstFrame = (columns->Columns.Size == 0); - if (columns->Columns.Size == 0) - { - columns->Columns.reserve(columns_count + 1); - for (int n = 0; n < columns_count + 1; n++) - { - ImGuiColumnData column; - column.OffsetNorm = n / (float) columns_count; - columns->Columns.push_back(column); - } - } - - for (int n = 0; n < columns_count + 1; n++) - { - // Clamp position - ImGuiColumnData *column = &columns->Columns[n]; - float t = column->OffsetNorm; - if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow)) - t = ImMin(t, PixelsToOffsetNorm(columns, (columns->MaxX - columns->MinX) - g.Style.ColumnsMinSpacing * (columns->Count - n))); - column->OffsetNorm = t; - - if (n == columns_count) - continue; - - // Compute clipping rectangle - float clip_x1 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n) - 1.0f); - float clip_x2 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n + 1) - 1.0f); - column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); - column->ClipRect.ClipWith(window->ClipRect); - } - - window->DrawList->ChannelsSplit(columns->Count); - PushColumnClipRect(); - PushItemWidth(GetColumnWidth() * 0.65f); -} - -void ImGui::EndColumns() -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - ImGuiColumnsSet *columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); - - PopItemWidth(); - PopClipRect(); - window->DrawList->ChannelsMerge(); - - columns->CellMaxY = ImMax(columns->CellMaxY, window->DC.CursorPos.y); - window->DC.CursorPos.y = columns->CellMaxY; - if (!(columns->Flags & ImGuiColumnsFlags_GrowParentContentsSize)) - window->DC.CursorMaxPos.x = ImMax(columns->StartMaxPosX, columns->MaxX); // Restore cursor max pos, as columns don't grow parent - - // Draw columns borders and handle resize - bool is_being_resized = false; - if (!(columns->Flags & ImGuiColumnsFlags_NoBorder) && !window->SkipItems) - { - const float y1 = columns->StartPosY; - const float y2 = window->DC.CursorPos.y; - int dragging_column = -1; - for (int n = 1; n < columns->Count; n++) - { - float x = window->Pos.x + GetColumnOffset(n); - const ImGuiID column_id = columns->ID + ImGuiID(n); - const float column_hw = GetColumnsRectHalfWidth(); // Half-width for interaction - const ImRect column_rect(ImVec2(x - column_hw, y1), ImVec2(x + column_hw, y2)); - KeepAliveID(column_id); - if (IsClippedEx(column_rect, column_id, false)) - continue; - - bool hovered = false, held = false; - if (!(columns->Flags & ImGuiColumnsFlags_NoResize)) - { - ButtonBehavior(column_rect, column_id, &hovered, &held); - if (hovered || held) - g.MouseCursor = ImGuiMouseCursor_ResizeEW; - if (held && !(columns->Columns[n].Flags & ImGuiColumnsFlags_NoResize)) - dragging_column = n; - } - - // Draw column (we clip the Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.) - const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : - ImGuiCol_Separator); - const float xi = (float) (int) x; - window->DrawList->AddLine(ImVec2(xi, ImMax(y1 + 1.0f, window->ClipRect.Min.y)), ImVec2(xi, ImMin(y2, window->ClipRect.Max.y)), col); - } - - // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. - if (dragging_column != -1) - { - if (!columns->IsBeingResized) - for (int n = 0; n < columns->Count + 1; n++) - columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; - columns->IsBeingResized = is_being_resized = true; - float x = GetDraggedColumnOffset(columns, dragging_column); - SetColumnOffset(dragging_column, x); - } - } - columns->IsBeingResized = is_being_resized; - - window->DC.ColumnsSet = NULL; - window->DC.ColumnsOffsetX = 0.0f; - window->DC.CursorPos.x = (float) (int) (window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); -} - -// [2017/12: This is currently the only public API, while we are working on making BeginColumns/EndColumns user-facing] -void ImGui::Columns(int columns_count, const char *id, bool border) -{ - ImGuiWindow *window = GetCurrentWindow(); - IM_ASSERT(columns_count >= 1); - if (window->DC.ColumnsSet != NULL && window->DC.ColumnsSet->Count != columns_count) - EndColumns(); - - ImGuiColumnsFlags flags = (border ? 0 : ImGuiColumnsFlags_NoBorder); - // flags |= ImGuiColumnsFlags_NoPreserveWidths; // NB: Legacy behavior - if (columns_count != 1) - BeginColumns(id, columns_count, flags); + // Note: SetKeyOwner() sets OwnerCurr. It is not strictly required for most mouse routing overlap (because of ActiveId/HoveredId + // are acting as filter before this has a chance to filter), but sane as soon as user tries to look into things. + // Setting OwnerCurr in SetKeyOwner() is more consistent than testing OwnerNext here: would be inconsistent with getter and other functions. + if (owner_data->OwnerCurr != owner_id) + { + if (owner_data->LockThisFrame) + return false; + if (owner_data->OwnerCurr != ImGuiKeyOwner_NoOwner) + return false; + } + + return true; } -void ImGui::Indent(float indent_w) +// _LockXXX flags are useful to lock keys away from code which is not input-owner aware. +// When using _LockXXX flags, you can use ImGuiKeyOwner_Any to lock keys from everyone. +// - SetKeyOwner(..., None) : clears owner +// - SetKeyOwner(..., Any, !Lock) : illegal (assert) +// - SetKeyOwner(..., Any or None, Lock) : set lock +void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - window->DC.IndentX += (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; - window->DC.CursorPos.x = window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX; + ImGuiContext& g = *GImGui; + IM_ASSERT(IsNamedKeyOrMod(key) && (owner_id != ImGuiKeyOwner_Any || (flags & (ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease)))); // Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it) + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetKeyOwner) == 0); // Passing flags not supported by this function! + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X, flags=%08X)\n", GetKeyName(key), owner_id, flags); + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + owner_data->OwnerCurr = owner_data->OwnerNext = owner_id; + + // We cannot lock by default as it would likely break lots of legacy code. + // In the case of using LockUntilRelease while key is not down we still lock during the frame (no key_data->Down test) + owner_data->LockUntilRelease = (flags & ImGuiInputFlags_LockUntilRelease) != 0; + owner_data->LockThisFrame = (flags & ImGuiInputFlags_LockThisFrame) != 0 || (owner_data->LockUntilRelease); } -void ImGui::Unindent(float indent_w) +// Rarely used helper +void ImGui::SetKeyOwnersForKeyChord(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = GetCurrentWindow(); - window->DC.IndentX -= (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; - window->DC.CursorPos.x = window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX; + if (key_chord & ImGuiMod_Ctrl) { SetKeyOwner(ImGuiMod_Ctrl, owner_id, flags); } + if (key_chord & ImGuiMod_Shift) { SetKeyOwner(ImGuiMod_Shift, owner_id, flags); } + if (key_chord & ImGuiMod_Alt) { SetKeyOwner(ImGuiMod_Alt, owner_id, flags); } + if (key_chord & ImGuiMod_Super) { SetKeyOwner(ImGuiMod_Super, owner_id, flags); } + if (key_chord & ~ImGuiMod_Mask_) { SetKeyOwner((ImGuiKey)(key_chord & ~ImGuiMod_Mask_), owner_id, flags); } } -void ImGui::TreePush(const char *str_id) +// This is more or less equivalent to: +// if (IsItemHovered() || IsItemActive()) +// SetKeyOwner(key, GetItemID()); +// Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times. +// More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition. +// Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority. +void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) { - ImGuiWindow *window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - PushID(str_id ? str_id : "#TreePush"); + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (id == 0 || (g.HoveredId != id && g.ActiveId != id)) + return; + if ((flags & ImGuiInputFlags_CondMask_) == 0) + flags |= ImGuiInputFlags_CondDefault_; + if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive))) + { + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function! + SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_); + } } -void ImGui::TreePush(const void *ptr_id) +void ImGui::SetItemKeyOwner(ImGuiKey key) { - ImGuiWindow *window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - PushID(ptr_id ? ptr_id : (const void *) "#TreePush"); + SetItemKeyOwner(key, ImGuiInputFlags_None); } -void ImGui::TreePushRawID(ImGuiID id) +// This is the only public API until we expose owner_id versions of the API as replacements. +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord) { - ImGuiWindow *window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - window->IDStack.push_back(id); + return IsKeyChordPressed(key_chord, ImGuiInputFlags_None, ImGuiKeyOwner_Any); } -void ImGui::TreePop() +// This is equivalent to comparing KeyMods + doing a IsKeyPressed() +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - Unindent(); - - window->DC.TreeDepth--; - if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) - if (g.NavIdIsAlive && (window->DC.TreeDepthMayJumpToParentOnPop & (1 << window->DC.TreeDepth))) - { - SetNavID(window->IDStack.back(), g.NavLayer); - NavMoveRequestCancel(); - } - window->DC.TreeDepthMayJumpToParentOnPop &= (1 << window->DC.TreeDepth) - 1; + ImGuiContext& g = *GImGui; + key_chord = FixupKeyChord(key_chord); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (g.IO.KeyMods != mods) + return false; - PopID(); + // Special storage location for mods + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + if (!IsKeyPressed(key, (flags & ImGuiInputFlags_RepeatMask_), owner_id)) + return false; + return true; } -void ImGui::Value(const char *prefix, bool b) +void ImGui::SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) { - Text("%s: %s", prefix, (b ? "true" : "false")); + ImGuiContext& g = *GImGui; + g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasShortcut; + g.NextItemData.Shortcut = key_chord; + g.NextItemData.ShortcutFlags = flags; } -void ImGui::Value(const char *prefix, int v) +// Called from within ItemAdd: at this point we can read from NextItemData and write to LastItemData +void ImGui::ItemHandleShortcut(ImGuiID id) { - Text("%s: %d", prefix, v); + ImGuiContext& g = *GImGui; + ImGuiInputFlags flags = g.NextItemData.ShortcutFlags; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetNextItemShortcut) == 0); // Passing flags not supported by SetNextItemShortcut()! + + if (g.LastItemData.InFlags & ImGuiItemFlags_Disabled) + return; + if (flags & ImGuiInputFlags_Tooltip) + { + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasShortcut; + g.LastItemData.Shortcut = g.NextItemData.Shortcut; + } + if (!Shortcut(g.NextItemData.Shortcut, flags & ImGuiInputFlags_SupportedByShortcut, id) || g.NavActivateId != 0) + return; + + // FIXME: Generalize Activation queue? + g.NavActivateId = id; // Will effectively disable clipping. + g.NavActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_FromShortcut; + //if (g.ActiveId == 0 || g.ActiveId == id) + g.NavActivateDownId = g.NavActivatePressedId = id; + NavHighlightActivated(id); } -void ImGui::Value(const char *prefix, unsigned int v) +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) { - Text("%s: %d", prefix, v); + return Shortcut(key_chord, flags, ImGuiKeyOwner_Any); } -void ImGui::Value(const char *prefix, float v, const char *float_format) +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) { - if (float_format) - { - char fmt[64]; - ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); - Text(fmt, prefix, v); - } - else - { - Text("%s: %.3f", prefix, v); - } + ImGuiContext& g = *GImGui; + //IMGUI_DEBUG_LOG("Shortcut(%s, flags=%X, owner_id=0x%08X)\n", GetKeyChordName(key_chord, g.TempBuffer.Data, g.TempBuffer.Size), flags, owner_id); + + // When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any. + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteFocused; + + // Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default) + // Effectively makes Shortcut() always input-owner aware. + if (owner_id == ImGuiKeyOwner_Any || owner_id == ImGuiKeyOwner_NoOwner) + owner_id = GetRoutingIdFromOwnerId(owner_id); + + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + return false; + + // Submit route + if (!SetShortcutRouting(key_chord, flags, owner_id)) + return false; + + // Default repeat behavior for Shortcut() + // So e.g. pressing Ctrl+W and releasing Ctrl while holding W will not trigger the W shortcut. + if ((flags & ImGuiInputFlags_Repeat) != 0 && (flags & ImGuiInputFlags_RepeatUntilMask_) == 0) + flags |= ImGuiInputFlags_RepeatUntilKeyModsChange; + + if (!IsKeyChordPressed(key_chord, flags, owner_id)) + return false; + + // Claim mods during the press + SetKeyOwnersForKeyChord(key_chord & ImGuiMod_Mask_, owner_id); + + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByShortcut) == 0); // Passing flags not supported by this function! + return true; } + //----------------------------------------------------------------------------- -// DRAG AND DROP +// [SECTION] ERROR CHECKING //----------------------------------------------------------------------------- -void ImGui::ClearDragDrop() -{ - ImGuiContext &g = *GImGui; - g.DragDropActive = false; - g.DragDropPayload.Clear(); - g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; - g.DragDropAcceptIdCurrRectSurface = FLT_MAX; - g.DragDropAcceptFrameCount = -1; +// Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues. +// Called by IMGUI_CHECKVERSION(). +// Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit +// If this triggers you have mismatched headers and compiled code versions. +// - It could be because of a build issue (using new headers with old compiled code) +// - It could be because of mismatched configuration #define, compilation settings, packing pragma etc. +// THE CONFIGURATION SETTINGS MENTIONED IN imconfig.h MUST BE SET FOR ALL COMPILATION UNITS INVOLVED WITH DEAR IMGUI. +// Which is why it is required you put them in your imconfig file (and NOT only before including imgui.h). +// Otherwise it is possible that different compilation units would see different structure layout. +// If you don't want to modify imconfig.h you can use the IMGUI_USER_CONFIG define to change filename. +bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) +{ + bool error = false; + if (strcmp(version, IMGUI_VERSION) != 0) { error = true; IM_ASSERT(strcmp(version, IMGUI_VERSION) == 0 && "Mismatched version string!"); } + if (sz_io != sizeof(ImGuiIO)) { error = true; IM_ASSERT(sz_io == sizeof(ImGuiIO) && "Mismatched struct layout!"); } + if (sz_style != sizeof(ImGuiStyle)) { error = true; IM_ASSERT(sz_style == sizeof(ImGuiStyle) && "Mismatched struct layout!"); } + if (sz_vec2 != sizeof(ImVec2)) { error = true; IM_ASSERT(sz_vec2 == sizeof(ImVec2) && "Mismatched struct layout!"); } + if (sz_vec4 != sizeof(ImVec4)) { error = true; IM_ASSERT(sz_vec4 == sizeof(ImVec4) && "Mismatched struct layout!"); } + if (sz_vert != sizeof(ImDrawVert)) { error = true; IM_ASSERT(sz_vert == sizeof(ImDrawVert) && "Mismatched struct layout!"); } + if (sz_idx != sizeof(ImDrawIdx)) { error = true; IM_ASSERT(sz_idx == sizeof(ImDrawIdx) && "Mismatched struct layout!"); } + return !error; +} + +// Until 1.89 (IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos() to extend the boundary of a parent (e.g. window or table cell) +// This is causing issues and ambiguity and we need to retire that. +// See https://github.com/ocornut/imgui/issues/5548 for more details. +// [Scenario 1] +// Previously this would make the window content size ~200x200: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK +// Instead, please submit an item: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); // OK +// Alternative: +// Begin(...) + Dummy(ImVec2(200,200)) + End(); // OK +// [Scenario 2] +// For reference this is one of the issue what we aim to fix with this change: +// BeginGroup() + SomeItem("foobar") + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup() +// The previous logic made SetCursorScreenPos(GetCursorScreenPos()) have a side-effect! It would erroneously incorporate ItemSpacing.y after the item into content size, making the group taller! +// While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect. Using vertical alignment patterns could trigger this issue. +void ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->DC.IsSetPos); + window->DC.IsSetPos = false; +#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window->DC.CursorPos.x <= window->DC.CursorMaxPos.x && window->DC.CursorPos.y <= window->DC.CursorMaxPos.y) + return; + if (window->SkipItems) + return; + IM_ASSERT(0 && "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries. Please submit an item e.g. Dummy() to validate extent."); +#else + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +#endif } -// Call when current ID is active. -// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() -bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) +static void ImGui::ErrorCheckNewFrameSanityChecks() { - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - - bool source_drag_active = false; - ImGuiID source_id = 0; - ImGuiID source_parent_id = 0; - int mouse_button = 0; - if (!(flags & ImGuiDragDropFlags_SourceExtern)) - { - source_id = window->DC.LastItemId; - if (source_id != 0 && g.ActiveId != source_id) // Early out for most common case - return false; - if (g.IO.MouseDown[mouse_button] == false) - return false; - - if (source_id == 0) - { - // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: - // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag, C) Swallow your programmer pride. - if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) - { - IM_ASSERT(0); - return false; - } - - // Magic fallback (=somehow reprehensible) to handle items with no assigned ID, e.g. Text(), Image() - // We build a throwaway ID based on current ID stack + relative AABB of items in window. - // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING OF THE WIDGET, so if your widget moves your dragging operation will be canceled. - // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. - bool is_hovered = (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) != 0; - if (!is_hovered && (g.ActiveId == 0 || g.ActiveIdWindow != window)) - return false; - source_id = window->DC.LastItemId = window->GetIDFromRectangle(window->DC.LastItemRect); - if (is_hovered) - SetHoveredID(source_id); - if (is_hovered && g.IO.MouseClicked[mouse_button]) - { - SetActiveID(source_id, window); - FocusWindow(window); - } - if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. - g.ActiveIdAllowOverlap = is_hovered; - } - if (g.ActiveId != source_id) - return false; - source_parent_id = window->IDStack.back(); - source_drag_active = IsMouseDragging(mouse_button); - } - else - { - window = NULL; - source_id = ImHash("#SourceExtern", 0); - source_drag_active = true; - } - - if (source_drag_active) - { - if (!g.DragDropActive) - { - IM_ASSERT(source_id != 0); - ClearDragDrop(); - ImGuiPayload &payload = g.DragDropPayload; - payload.SourceId = source_id; - payload.SourceParentId = source_parent_id; - g.DragDropActive = true; - g.DragDropSourceFlags = flags; - g.DragDropMouseButton = mouse_button; - } - - if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - // FIXME-DRAG - // SetNextWindowPos(g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding); - // PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This is better but e.g ColorButton with checkboard has issue with transparent colors :( - SetNextWindowPos(g.IO.MousePos); - PushStyleColor(ImGuiCol_PopupBg, GetStyleColorVec4(ImGuiCol_PopupBg) * ImVec4(1.0f, 1.0f, 1.0f, 0.6f)); - BeginTooltip(); - } - - if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) - window->DC.LastItemStatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; - - return true; - } - return false; -} + ImGuiContext& g = *GImGui; -void ImGui::EndDragDropSource() -{ - ImGuiContext &g = *GImGui; - IM_ASSERT(g.DragDropActive); + // Check user IM_ASSERT macro + // (IF YOU GET A WARNING OR COMPILE ERROR HERE: it means your assert macro is incorrectly defined! + // If your macro uses multiple statements, it NEEDS to be surrounded by a 'do { ... } while (0)' block. + // This is a common C/C++ idiom to allow multiple statements macros to be used in control flow blocks.) + // #define IM_ASSERT(EXPR) if (SomeCode(EXPR)) SomeMoreCode(); // Wrong! + // #define IM_ASSERT(EXPR) do { if (SomeCode(EXPR)) SomeMoreCode(); } while (0) // Correct! + if (true) IM_ASSERT(1); else IM_ASSERT(0); - if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - EndTooltip(); - PopStyleColor(); - // PopStyleVar(); - } + // Emscripten backends are often imprecise in their submission of DeltaTime. (#6114, #3644) + // Ideally the Emscripten app/backend should aim to fix or smooth this value and avoid feeding zero, but we tolerate it. +#ifdef __EMSCRIPTEN__ + if (g.IO.DeltaTime <= 0.0f && g.FrameCount > 0) + g.IO.DeltaTime = 0.00001f; +#endif - // Discard the drag if have not called SetDragDropPayload() - if (g.DragDropPayload.DataFrameCount == -1) - ClearDragDrop(); -} + // Check user data + // (We pass an error message in the assert expression to make it visible to programmers who are not using a debugger, as most assert handlers display their argument) + IM_ASSERT(g.Initialized); + IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!"); + IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); + IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value!"); + IM_ASSERT(g.IO.Fonts->IsBuilt() && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()"); + IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations + IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting."); + IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); + IM_ASSERT(g.Style.ColorButtonPosition == ImGuiDir_Left || g.Style.ColorButtonPosition == ImGuiDir_Right); +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + for (int n = ImGuiKey_NamedKey_BEGIN; n < ImGuiKey_COUNT; n++) + IM_ASSERT(g.IO.KeyMap[n] >= -1 && g.IO.KeyMap[n] < ImGuiKey_LegacyNativeKey_END && "io.KeyMap[] contains an out of bound value (need to be 0..511, or -1 for unmapped key)"); + + // Check: required key mapping (we intentionally do NOT check all keys to not pressure user into setting up everything, but Space is required and was only added in 1.60 WIP) + if ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && g.IO.BackendUsingLegacyKeyArrays == 1) + IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); +#endif -// Use 'cond' to choose to submit payload on drag start or every frame -bool ImGui::SetDragDropPayload(const char *type, const void *data, size_t data_size, ImGuiCond cond) -{ - ImGuiContext &g = *GImGui; - ImGuiPayload &payload = g.DragDropPayload; - if (cond == 0) - cond = ImGuiCond_Always; - - IM_ASSERT(type != NULL); - IM_ASSERT(strlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 12 characters long"); - IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); - IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); - IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() - - if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) - { - // Copy payload - ImStrncpy(payload.DataType, type, IM_ARRAYSIZE(payload.DataType)); - g.DragDropPayloadBufHeap.resize(0); - if (data_size > sizeof(g.DragDropPayloadBufLocal)) - { - // Store in heap - g.DragDropPayloadBufHeap.resize((int) data_size); - payload.Data = g.DragDropPayloadBufHeap.Data; - memcpy((void *) payload.Data, data, data_size); - } - else if (data_size > 0) - { - // Store locally - memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); - payload.Data = g.DragDropPayloadBufLocal; - memcpy((void *) payload.Data, data, data_size); - } - else - { - payload.Data = NULL; - } - payload.DataSize = (int) data_size; - } - payload.DataFrameCount = g.FrameCount; - - return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); -} - -bool ImGui::BeginDragDropTargetCustom(const ImRect &bb, ImGuiID id) -{ - ImGuiContext &g = *GImGui; - if (!g.DragDropActive) - return false; - - ImGuiWindow *window = g.CurrentWindow; - if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) - return false; - IM_ASSERT(id != 0); - if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) - return false; - - g.DragDropTargetRect = bb; - g.DragDropTargetId = id; - return true; + // Remap legacy clipboard handlers (OBSOLETED in 1.91.1, August 2024) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (g.IO.GetClipboardTextFn != NULL && (g.PlatformIO.Platform_GetClipboardTextFn == NULL || g.PlatformIO.Platform_GetClipboardTextFn == Platform_GetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_GetClipboardTextFn = [](ImGuiContext* ctx) { return ctx->IO.GetClipboardTextFn(ctx->IO.ClipboardUserData); }; + if (g.IO.SetClipboardTextFn != NULL && (g.PlatformIO.Platform_SetClipboardTextFn == NULL || g.PlatformIO.Platform_SetClipboardTextFn == Platform_SetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_SetClipboardTextFn = [](ImGuiContext* ctx, const char* text) { return ctx->IO.SetClipboardTextFn(ctx->IO.ClipboardUserData, text); }; +#endif } -// We don't use BeginDragDropTargetCustom() and duplicate its code because: -// 1) we use LastItemRectHoveredRect which handles items that pushes a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. -// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. -// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) -bool ImGui::BeginDragDropTarget() +static void ImGui::ErrorCheckEndFrameSanityChecks() { - ImGuiContext &g = *GImGui; - if (!g.DragDropActive) - return false; + ImGuiContext& g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) - return false; - if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) - return false; + // Verify that io.KeyXXX fields haven't been tampered with. Key mods should not be modified between NewFrame() and EndFrame() + // One possible reason leading to this assert is that your backends update inputs _AFTER_ NewFrame(). + // It is known that when some modal native windows called mid-frame takes focus away, some backends such as GLFW will + // send key release events mid-frame. This would normally trigger this assertion and lead to sheared inputs. + // We silently accommodate for this case by ignoring the case where all io.KeyXXX modifiers were released (aka key_mod_flags == 0), + // while still correctly asserting on mid-frame key press events. + const ImGuiKeyChord key_mods = GetMergedModsFromKeys(); + IM_ASSERT((key_mods == 0 || g.IO.KeyMods == key_mods) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); + IM_UNUSED(key_mods); - const ImRect &display_rect = (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? window->DC.LastItemDisplayRect : window->DC.LastItemRect; - ImGuiID id = window->DC.LastItemId; - if (id == 0) - id = window->GetIDFromRectangle(display_rect); - if (g.DragDropPayload.SourceId == id) - return false; + // [EXPERIMENTAL] Recover from errors: You may call this yourself before EndFrame(). + //ErrorCheckEndFrameRecover(); + + // Report when there is a mismatch of Begin/BeginChild vs End/EndChild calls. Important: Remember that the Begin/BeginChild API requires you + // to always call End/EndChild even if Begin/BeginChild returns false! (this is unfortunately inconsistent with most other Begin* API). + if (g.CurrentWindowStack.Size != 1) + { + if (g.CurrentWindowStack.Size > 1) + { + ImGuiWindow* window = g.CurrentWindowStack.back().Window; // <-- This window was not Ended! + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?"); + IM_UNUSED(window); + while (g.CurrentWindowStack.Size > 1) + End(); + } + else + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?"); + } + } - g.DragDropTargetRect = display_rect; - g.DragDropTargetId = id; - return true; + IM_ASSERT_USER_ERROR(g.GroupStack.Size == 0, "Missing EndGroup call!"); } -bool ImGui::IsDragDropPayloadBeingAccepted() -{ - ImGuiContext &g = *GImGui; - return g.DragDropActive && g.DragDropAcceptIdPrev != 0; -} - -const ImGuiPayload *ImGui::AcceptDragDropPayload(const char *type, ImGuiDragDropFlags flags) -{ - ImGuiContext &g = *GImGui; - ImGuiWindow *window = g.CurrentWindow; - ImGuiPayload &payload = g.DragDropPayload; - IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? - IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? - if (type != NULL && !payload.IsDataType(type)) - return NULL; - - // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. - // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! - const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); - ImRect r = g.DragDropTargetRect; - float r_surface = r.GetWidth() * r.GetHeight(); - if (r_surface < g.DragDropAcceptIdCurrRectSurface) - { - g.DragDropAcceptIdCurr = g.DragDropTargetId; - g.DragDropAcceptIdCurrRectSurface = r_surface; - } - - // Render default drop visuals - payload.Preview = was_accepted_previously; - flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that lives for 1 frame) - if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) - { - // FIXME-DRAG: Settle on a proper default visuals for drop target. - r.Expand(3.5f); - bool push_clip_rect = !window->ClipRect.Contains(r); - if (push_clip_rect) - window->DrawList->PushClipRectFullScreen(); - window->DrawList->AddRect(r.Min, r.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, ~0, 2.0f); - if (push_clip_rect) - window->DrawList->PopClipRect(); - } - - g.DragDropAcceptFrameCount = g.FrameCount; - payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting os window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() - if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) - return NULL; - - return &payload; -} - -// We don't really use/need this now, but added it for the sake of consistency and because we might need it later. -void ImGui::EndDragDropTarget() +// Experimental recovery from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. +// Must be called during or before EndFrame(). +// This is generally flawed as we are not necessarily End/Popping things in the right order. +// FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. +// FIXME: Can't recover from interleaved BeginTabBar/Begin +void ImGui::ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data) { - ImGuiContext &g = *GImGui; - (void) g; - IM_ASSERT(g.DragDropActive); + // PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations" + ImGuiContext& g = *GImGui; + while (g.CurrentWindowStack.Size > 0) //-V1044 + { + ErrorCheckEndWindowRecover(log_callback, user_data); + ImGuiWindow* window = g.CurrentWindow; + if (g.CurrentWindowStack.Size == 1) + { + IM_ASSERT(window->IsFallbackWindow); + break; + } + if (window->Flags & ImGuiWindowFlags_ChildWindow) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndChild() for '%s'", window->Name); + EndChild(); + } + else + { + if (log_callback) log_callback(user_data, "Recovered from missing End() for '%s'", window->Name); + End(); + } + } } -//----------------------------------------------------------------------------- -// PLATFORM DEPENDENT HELPERS -//----------------------------------------------------------------------------- - -#if defined(_WIN32) && !defined(_WINDOWS_) && (!defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) || !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS)) -# undef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN -# ifndef __MINGW32__ -# include -# else -# include -# endif -#endif +// Must be called before End()/EndChild() +void ImGui::ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data) +{ + ImGuiContext& g = *GImGui; + while (g.CurrentTable && (g.CurrentTable->OuterWindow == g.CurrentWindow || g.CurrentTable->InnerWindow == g.CurrentWindow)) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndTable() in '%s'", g.CurrentTable->OuterWindow->Name); + EndTable(); + } -// Win32 API clipboard implementation -#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) - -# ifdef _MSC_VER -# pragma comment(lib, "user32") -# endif - -static const char *GetClipboardTextFn_DefaultImpl(void *) -{ - static ImVector buf_local; - buf_local.clear(); - if (!OpenClipboard(NULL)) - return NULL; - HANDLE wbuf_handle = GetClipboardData(CF_UNICODETEXT); - if (wbuf_handle == NULL) - { - CloseClipboard(); - return NULL; - } - if (ImWchar *wbuf_global = (ImWchar *) GlobalLock(wbuf_handle)) - { - int buf_len = ImTextCountUtf8BytesFromStr(wbuf_global, NULL) + 1; - buf_local.resize(buf_len); - ImTextStrToUtf8(buf_local.Data, buf_len, wbuf_global, NULL); - } - GlobalUnlock(wbuf_handle); - CloseClipboard(); - return buf_local.Data; -} - -static void SetClipboardTextFn_DefaultImpl(void *, const char *text) -{ - if (!OpenClipboard(NULL)) - return; - const int wbuf_length = ImTextCountCharsFromUtf8(text, NULL) + 1; - HGLOBAL wbuf_handle = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T) wbuf_length * sizeof(ImWchar)); - if (wbuf_handle == NULL) - { - CloseClipboard(); - return; - } - ImWchar *wbuf_global = (ImWchar *) GlobalLock(wbuf_handle); - ImTextStrFromUtf8(wbuf_global, wbuf_length, text, NULL); - GlobalUnlock(wbuf_handle); - EmptyClipboard(); - SetClipboardData(CF_UNICODETEXT, wbuf_handle); - CloseClipboard(); + ImGuiWindow* window = g.CurrentWindow; + ImGuiStackSizes* stack_sizes = &g.CurrentWindowStack.back().StackSizesOnBegin; + IM_ASSERT(window != NULL); + while (g.CurrentTabBar != NULL) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndTabBar() in '%s'", window->Name); + EndTabBar(); + } + while (g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Storage->Window == window) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndMultiSelect() in '%s'", window->Name); + EndMultiSelect(); + } + while (window->DC.TreeDepth > 0) + { + if (log_callback) log_callback(user_data, "Recovered from missing TreePop() in '%s'", window->Name); + TreePop(); + } + while (g.GroupStack.Size > stack_sizes->SizeOfGroupStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndGroup() in '%s'", window->Name); + EndGroup(); + } + while (window->IDStack.Size > 1) + { + if (log_callback) log_callback(user_data, "Recovered from missing PopID() in '%s'", window->Name); + PopID(); + } + while (g.DisabledStackSize > stack_sizes->SizeOfDisabledStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing EndDisabled() in '%s'", window->Name); + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + EndDisabled(); + else + { + EndDisabledOverrideReenable(); + g.CurrentWindowStack.back().DisabledOverrideReenable = false; + } + } + while (g.ColorStack.Size > stack_sizes->SizeOfColorStack) + { + if (log_callback) log_callback(user_data, "Recovered from missing PopStyleColor() in '%s' for ImGuiCol_%s", window->Name, GetStyleColorName(g.ColorStack.back().Col)); + PopStyleColor(); + } + while (g.ItemFlagsStack.Size > stack_sizes->SizeOfItemFlagsStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopItemFlag() in '%s'", window->Name); + PopItemFlag(); + } + while (g.StyleVarStack.Size > stack_sizes->SizeOfStyleVarStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopStyleVar() in '%s'", window->Name); + PopStyleVar(); + } + while (g.FontStack.Size > stack_sizes->SizeOfFontStack) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopFont() in '%s'", window->Name); + PopFont(); + } + while (g.FocusScopeStack.Size > stack_sizes->SizeOfFocusScopeStack + 1) //-V1044 + { + if (log_callback) log_callback(user_data, "Recovered from missing PopFocusScope() in '%s'", window->Name); + PopFocusScope(); + } } -#else - -// Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers -static const char *GetClipboardTextFn_DefaultImpl(void *) +// Save current stack sizes for later compare +void ImGuiStackSizes::SetToContextState(ImGuiContext* ctx) { - ImGuiContext &g = *GImGui; - return g.PrivateClipboard.empty() ? NULL : g.PrivateClipboard.begin(); + ImGuiContext& g = *ctx; + ImGuiWindow* window = g.CurrentWindow; + SizeOfIDStack = (short)window->IDStack.Size; + SizeOfColorStack = (short)g.ColorStack.Size; + SizeOfStyleVarStack = (short)g.StyleVarStack.Size; + SizeOfFontStack = (short)g.FontStack.Size; + SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; + SizeOfGroupStack = (short)g.GroupStack.Size; + SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; + SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; + SizeOfDisabledStack = (short)g.DisabledStackSize; } -// Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers -static void SetClipboardTextFn_DefaultImpl(void *, const char *text) +// Compare to detect usage errors +void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx) { - ImGuiContext &g = *GImGui; - g.PrivateClipboard.clear(); - const char *text_end = text + strlen(text); - g.PrivateClipboard.resize((int) (text_end - text) + 1); - memcpy(&g.PrivateClipboard[0], text, (size_t) (text_end - text)); - g.PrivateClipboard[(int) (text_end - text)] = 0; -} + ImGuiContext& g = *ctx; + ImGuiWindow* window = g.CurrentWindow; + IM_UNUSED(window); -#endif + // Window stacks + // NOT checking: DC.ItemWidth, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) + IM_ASSERT(SizeOfIDStack == window->IDStack.Size && "PushID/PopID or TreeNode/TreePop Mismatch!"); -// Win32 API IME support (for Asian languages, etc.) -#if defined(_WIN32) && !defined(__GNUC__) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) + // Global stacks + // For color, style and font stacks there is an incentive to use Push/Begin/Pop/.../End patterns, so we relax our checks a little to allow them. + IM_ASSERT(SizeOfGroupStack == g.GroupStack.Size && "BeginGroup/EndGroup Mismatch!"); + IM_ASSERT(SizeOfBeginPopupStack == g.BeginPopupStack.Size && "BeginPopup/EndPopup or BeginMenu/EndMenu Mismatch!"); + IM_ASSERT(SizeOfDisabledStack == g.DisabledStackSize && "BeginDisabled/EndDisabled Mismatch!"); + IM_ASSERT(SizeOfItemFlagsStack >= g.ItemFlagsStack.Size && "PushItemFlag/PopItemFlag Mismatch!"); + IM_ASSERT(SizeOfColorStack >= g.ColorStack.Size && "PushStyleColor/PopStyleColor Mismatch!"); + IM_ASSERT(SizeOfStyleVarStack >= g.StyleVarStack.Size && "PushStyleVar/PopStyleVar Mismatch!"); + IM_ASSERT(SizeOfFontStack >= g.FontStack.Size && "PushFont/PopFont Mismatch!"); + IM_ASSERT(SizeOfFocusScopeStack == g.FocusScopeStack.Size && "PushFocusScope/PopFocusScope Mismatch!"); +} -# include -# ifdef _MSC_VER -# pragma comment(lib, "imm32") -# endif +//----------------------------------------------------------------------------- +// [SECTION] ITEM SUBMISSION +//----------------------------------------------------------------------------- +// - KeepAliveID() +// - ItemAdd() +//----------------------------------------------------------------------------- -static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y) +// Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID(). +void ImGui::KeepAliveID(ImGuiID id) { - // Notify OS Input Method Editor of text input position - if (HWND hwnd = (HWND) GImGui->IO.ImeWindowHandle) - if (HIMC himc = ImmGetContext(hwnd)) - { - COMPOSITIONFORM cf; - cf.ptCurrentPos.x = x; - cf.ptCurrentPos.y = y; - cf.dwStyle = CFS_FORCE_POSITION; - ImmSetCompositionWindow(himc, &cf); - } + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = id; + if (g.ActiveIdPreviousFrame == id) + g.ActiveIdPreviousFrameIsAlive = true; } -#else +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. +// THIS IS IN THE PERFORMANCE CRITICAL PATH (UNTIL THE CLIPPING TEST AND EARLY-RETURN) +IM_MSVC_RUNTIME_CHECKS_OFF +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Set item data + // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) + g.LastItemData.ID = id; + g.LastItemData.Rect = bb; + g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; + g.LastItemData.InFlags = g.CurrentItemFlags | g.NextItemData.ItemFlags | extra_flags; + g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; + // Note: we don't copy 'g.NextItemData.SelectionUserData' to an hypothetical g.LastItemData.SelectionUserData: since the former is not cleared. + + if (id != 0) + { + KeepAliveID(id); + + // Directional navigation processing + // Runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests + // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of + // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able + // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). + // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. + // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. + if (!(g.LastItemData.InFlags & ImGuiItemFlags_NoNav)) + { + // FIMXE-NAV: investigate changing the window tests into a simple 'if (g.NavFocusScopeId == g.CurrentFocusScopeId)' test. + window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened)) + NavProcessItem(); + } + + if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasShortcut) + ItemHandleShortcut(id); + } -static void ImeSetInputScreenPosFn_DefaultImpl(int, int) -{} + // Lightweight clear of SetNextItemXXX data. + g.NextItemData.Flags = ImGuiNextItemDataFlags_None; + g.NextItemData.ItemFlags = ImGuiItemFlags_None; +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0) + IMGUI_TEST_ENGINE_ITEM_ADD(id, g.LastItemData.NavRect, &g.LastItemData); #endif -//----------------------------------------------------------------------------- -// HELP -//----------------------------------------------------------------------------- + // Clipping test + // (this is an inline copy of IsClippedEx() so we can reuse the is_rect_visible value, otherwise we'd do 'if (IsClippedEx(bb, id)) return false') + // g.NavActivateId is not necessarily == g.NavId, in the case of remote activation (e.g. shortcuts) + const bool is_rect_visible = bb.Overlaps(window->ClipRect); + if (!is_rect_visible) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return false; + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + if (id == g.DebugLocateId) + DebugLocateItemResolveWithLastItem(); + + // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". + // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". + // READ THE FAQ: https://dearimgui.com/faq + IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); + } + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + //if ((g.LastItemData.InFlags & ImGuiItemFlags_NoNav) == 0) + // window->DrawList->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, IM_COL32(255,255,0,255)); // [DEBUG] +#endif -void ImGui::ShowMetricsWindow(bool *p_open) -{ - if (ImGui::Begin("ImGui Metrics", p_open)) - { - ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - ImGui::Text("%d vertices, %d indices (%d triangles)", ImGui::GetIO().MetricsRenderVertices, ImGui::GetIO().MetricsRenderIndices, ImGui::GetIO().MetricsRenderIndices / 3); - ImGui::Text("%d allocations", (int) GImAllocatorActiveAllocationsCount); - static bool show_clip_rects = true; - ImGui::Checkbox("Show clipping rectangles when hovering draw commands", &show_clip_rects); - ImGui::Separator(); - - struct Funcs - { - static void NodeDrawList(ImGuiWindow *window, ImDrawList *draw_list, const char *label) - { - bool node_open = ImGui::TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, draw_list->CmdBuffer.Size); - if (draw_list == ImGui::GetWindowDrawList()) - { - ImGui::SameLine(); - ImGui::TextColored(ImColor(255, 100, 100), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) - if (node_open) - ImGui::TreePop(); - return; - } - - ImDrawList *overlay_draw_list = ImGui::GetOverlayDrawList(); // Render additional visuals into the top-most draw list - if (window && ImGui::IsItemHovered()) - overlay_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); - if (!node_open) - return; - - int elem_offset = 0; - for (const ImDrawCmd *pcmd = draw_list->CmdBuffer.begin(); pcmd < draw_list->CmdBuffer.end(); elem_offset += pcmd->ElemCount, pcmd++) - { - if (pcmd->UserCallback == NULL && pcmd->ElemCount == 0) - continue; - if (pcmd->UserCallback) - { - ImGui::BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); - continue; - } - ImDrawIdx *idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; - bool pcmd_node_open = ImGui::TreeNode((void *) (pcmd - draw_list->CmdBuffer.begin()), "Draw %4d %s vtx, tex 0x%p, clip_rect (%4.0f,%4.0f)-(%4.0f,%4.0f)", pcmd->ElemCount, draw_list->IdxBuffer.Size > 0 ? "indexed" : "non-indexed", pcmd->TextureId, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); - if (show_clip_rects && ImGui::IsItemHovered()) - { - ImRect clip_rect = pcmd->ClipRect; - ImRect vtxs_rect; - for (int i = elem_offset; i < elem_offset + (int) pcmd->ElemCount; i++) - vtxs_rect.Add(draw_list->VtxBuffer[idx_buffer ? idx_buffer[i] : i].pos); - clip_rect.Floor(); - overlay_draw_list->AddRect(clip_rect.Min, clip_rect.Max, IM_COL32(255, 255, 0, 255)); - vtxs_rect.Floor(); - overlay_draw_list->AddRect(vtxs_rect.Min, vtxs_rect.Max, IM_COL32(255, 0, 255, 255)); - } - if (!pcmd_node_open) - continue; - - // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. - ImGuiListClipper clipper(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. - while (clipper.Step()) - for (int prim = clipper.DisplayStart, vtx_i = elem_offset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) - { - char buf[300]; - char *buf_p = buf, *buf_end = buf + IM_ARRAYSIZE(buf); - ImVec2 triangles_pos[3]; - for (int n = 0; n < 3; n++, vtx_i++) - { - ImDrawVert &v = draw_list->VtxBuffer[idx_buffer ? idx_buffer[vtx_i] : vtx_i]; - triangles_pos[n] = v.pos; - buf_p += ImFormatString(buf_p, (int) (buf_end - buf_p), "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", (n == 0) ? "vtx" : " ", vtx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); - } - ImGui::Selectable(buf, false); - if (ImGui::IsItemHovered()) - { - ImDrawListFlags backup_flags = overlay_draw_list->Flags; - overlay_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines at is more readable for very large and thin triangles. - overlay_draw_list->AddPolyline(triangles_pos, 3, IM_COL32(255, 255, 0, 255), true, 1.0f); - overlay_draw_list->Flags = backup_flags; - } - } - ImGui::TreePop(); - } - ImGui::TreePop(); - } - - static void NodeWindows(ImVector &windows, const char *label) - { - if (!ImGui::TreeNode(label, "%s (%d)", label, windows.Size)) - return; - for (int i = 0; i < windows.Size; i++) - Funcs::NodeWindow(windows[i], "Window"); - ImGui::TreePop(); - } - - static void NodeWindow(ImGuiWindow *window, const char *label) - { - if (!ImGui::TreeNode(window, "%s '%s', %d @ 0x%p", label, window->Name, window->Active || window->WasActive, window)) - return; - ImGuiWindowFlags flags = window->Flags; - NodeDrawList(window, window->DrawList, "DrawList"); - ImGui::BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), SizeContents (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->SizeContents.x, window->SizeContents.y); - ImGui::BulletText("Flags: 0x%08X (%s%s%s%s%s%s..)", flags, - (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", - (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : ""); - ImGui::BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f)", window->Scroll.x, GetScrollMaxX(window), window->Scroll.y, GetScrollMaxY(window)); - ImGui::BulletText("Active: %d, WriteAccessed: %d", window->Active, window->WriteAccessed); - ImGui::BulletText("NavLastIds: 0x%08X,0x%08X, NavLayerActiveMask: %X", window->NavLastIds[0], window->NavLastIds[1], window->DC.NavLayerActiveMask); - ImGui::BulletText("NavLastChildNavWindow: %s", window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); - if (window->NavRectRel[0].IsFinite()) - ImGui::BulletText("NavRectRel[0]: (%.1f,%.1f)(%.1f,%.1f)", window->NavRectRel[0].Min.x, window->NavRectRel[0].Min.y, window->NavRectRel[0].Max.x, window->NavRectRel[0].Max.y); - else - ImGui::BulletText("NavRectRel[0]: "); - if (window->RootWindow != window) - NodeWindow(window->RootWindow, "RootWindow"); - if (window->DC.ChildWindows.Size > 0) - NodeWindows(window->DC.ChildWindows, "ChildWindows"); - ImGui::BulletText("Storage: %d bytes", window->StateStorage.Data.Size * (int) sizeof(ImGuiStorage::Pair)); - ImGui::TreePop(); - } - }; - - // Access private state, we are going to display the draw lists from last frame - ImGuiContext &g = *GImGui; - Funcs::NodeWindows(g.Windows, "Windows"); - if (ImGui::TreeNode("DrawList", "Active DrawLists (%d)", g.DrawDataBuilder.Layers[0].Size)) - { - for (int i = 0; i < g.DrawDataBuilder.Layers[0].Size; i++) - Funcs::NodeDrawList(NULL, g.DrawDataBuilder.Layers[0][i], "DrawList"); - ImGui::TreePop(); - } - if (ImGui::TreeNode("Popups", "Open Popups Stack (%d)", g.OpenPopupStack.Size)) - { - for (int i = 0; i < g.OpenPopupStack.Size; i++) - { - ImGuiWindow *window = g.OpenPopupStack[i].Window; - ImGui::BulletText("PopupID: %08x, Window: '%s'%s%s", g.OpenPopupStack[i].PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? " ChildWindow" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? " ChildMenu" : ""); - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Internal state")) - { - const char *input_source_names[] = {"None", "Mouse", "Nav", "NavGamepad", "NavKeyboard"}; - IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_Count_); - ImGui::Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); - ImGui::Text("HoveredRootWindow: '%s'", g.HoveredRootWindow ? g.HoveredRootWindow->Name : "NULL"); - ImGui::Text("HoveredId: 0x%08X/0x%08X (%.2f sec)", g.HoveredId, g.HoveredIdPreviousFrame, g.HoveredIdTimer); // Data is "in-flight" so depending on when the Metrics window is called we may see current frame information or not - ImGui::Text("ActiveId: 0x%08X/0x%08X (%.2f sec), ActiveIdSource: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, input_source_names[g.ActiveIdSource]); - ImGui::Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); - ImGui::Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); - ImGui::Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); - ImGui::Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); - ImGui::Text("NavActivateId: 0x%08X, NavInputId: 0x%08X", g.NavActivateId, g.NavInputId); - ImGui::Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); - ImGui::Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); - ImGui::TreePop(); - } - } - ImGui::End(); + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (is_rect_visible) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Visible; + if (IsMouseHoveringRect(bb.Min, bb.Max)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; } +IM_MSVC_RUNTIME_CHECKS_RESTORE +//----------------------------------------------------------------------------- +// [SECTION] LAYOUT +//----------------------------------------------------------------------------- +// - ItemSize() +// - SameLine() +// - GetCursorScreenPos() +// - SetCursorScreenPos() +// - GetCursorPos(), GetCursorPosX(), GetCursorPosY() +// - SetCursorPos(), SetCursorPosX(), SetCursorPosY() +// - GetCursorStartPos() +// - Indent() +// - Unindent() +// - SetNextItemWidth() +// - PushItemWidth() +// - PushMultiItemsWidths() +// - PopItemWidth() +// - CalcItemWidth() +// - CalcItemSize() +// - GetTextLineHeight() +// - GetTextLineHeightWithSpacing() +// - GetFrameHeight() +// - GetFrameHeightWithSpacing() +// - GetContentRegionMax() +// - GetContentRegionAvail(), +// - BeginGroup() +// - EndGroup() +// Also see in imgui_widgets: tab bars, and in imgui_tables: tables, columns. //----------------------------------------------------------------------------- -// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. -// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. -#ifdef IMGUI_INCLUDE_IMGUI_USER_INL -# include "imgui_user.inl" -#endif +// Advance cursor given item size for layout. +// Register minimum needed size so it can extend the bounding box used for auto-fit calculation. +// See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different. +// THIS IS IN THE PERFORMANCE CRITICAL PATH. +IM_MSVC_RUNTIME_CHECKS_OFF +void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // We increase the height in this function to accommodate for baseline offset. + // In theory we should be offsetting the starting position (window->DC.CursorPos), that will be the topic of a larger refactor, + // but since ItemSize() is not yet an API that moves the cursor (to handle e.g. wrapping) enlarging the height has the same effect. + const float offset_to_match_baseline_y = (text_baseline_y >= 0) ? ImMax(0.0f, window->DC.CurrLineTextBaseOffset - text_baseline_y) : 0.0f; + + const float line_y1 = window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y; + const float line_height = ImMax(window->DC.CurrLineSize.y, /*ImMax(*/window->DC.CursorPos.y - line_y1/*, 0.0f)*/ + size.y + offset_to_match_baseline_y); + + // Always align ourselves on pixel boundaries + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; + window->DC.CursorPosPrevLine.y = line_y1; + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line + window->DC.CursorPos.y = IM_TRUNC(line_y1 + line_height + g.Style.ItemSpacing.y); // Next line + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineSize.y = line_height; + window->DC.CurrLineSize.y = 0.0f; + window->DC.PrevLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, text_baseline_y); + window->DC.CurrLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE -//----------------------------------------------------------------------------- +// Gets back to previous line and continue with horizontal layout +// offset_from_start_x == 0 : follow right after previous item +// offset_from_start_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if offset_from_start_x == 0, no spacing if offset_from_start_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float offset_from_start_x, float spacing_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + if (offset_from_start_x != 0.0f) + { + if (spacing_w < 0.0f) + spacing_w = 0.0f; + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + offset_from_start_x + spacing_w + window->DC.GroupOffset.x + window->DC.ColumnsOffset.x; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + else + { + if (spacing_w < 0.0f) + spacing_w = g.Style.ItemSpacing.x; + window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + window->DC.CurrLineSize = window->DC.PrevLineSize; + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + window->DC.IsSameLine = true; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +void ImGui::SetCursorScreenPos(const ImVec2& pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = pos; + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; +} + +// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. +// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. +ImVec2 ImGui::GetCursorPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos - window->Pos + window->Scroll; +} + +float ImGui::GetCursorPosX() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; +} + +float ImGui::GetCursorPosY() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; +} + +void ImGui::SetCursorPos(const ImVec2& local_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; +} + +void ImGui::SetCursorPosX(float x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; + //window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); + window->DC.IsSetPos = true; +} + +void ImGui::SetCursorPosY(float y) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; + //window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); + window->DC.IsSetPos = true; +} + +ImVec2 ImGui::GetCursorStartPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; +} + +void ImGui::Indent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x += (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +void ImGui::Unindent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x -= (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +// Affect large frame+labels widgets only. +void ImGui::SetNextItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasWidth; + g.NextItemData.Width = item_width; +} + +// FIXME: Remove the == 0.0f behavior? +void ImGui::PushItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PushMultiItemsWidths(int components, float w_full) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(components > 0); + const ImGuiStyle& style = g.Style; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + float w_items = w_full - style.ItemInnerSpacing.x * (components - 1); + float prev_split = w_items; + for (int i = components - 1; i > 0; i--) + { + float next_split = IM_TRUNC(w_items * i / components); + window->DC.ItemWidthStack.push_back(ImMax(prev_split - next_split, 1.0f)); + prev_split = next_split; + } + window->DC.ItemWidth = ImMax(prev_split, 1.0f); + g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PopItemWidth() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.ItemWidth = window->DC.ItemWidthStack.back(); + window->DC.ItemWidthStack.pop_back(); +} + +// Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth(). +// The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags() +float ImGui::CalcItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float w; + if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth) + w = g.NextItemData.Width; + else + w = window->DC.ItemWidth; + if (w < 0.0f) + { + float region_avail_x = GetContentRegionAvail().x; + w = ImMax(1.0f, region_avail_x + w); + } + w = IM_TRUNC(w); + return w; +} + +// [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth(). +// Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical. +// Note that only CalcItemWidth() is publicly exposed. +// The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable) +ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_w, float default_h) +{ + ImVec2 avail; + if (size.x < 0.0f || size.y < 0.0f) + avail = GetContentRegionAvail(); + + if (size.x == 0.0f) + size.x = default_w; + else if (size.x < 0.0f) + size.x = ImMax(4.0f, avail.x + size.x); // <-- size.x is negative here so we are subtracting + + if (size.y == 0.0f) + size.y = default_h; + else if (size.y < 0.0f) + size.y = ImMax(4.0f, avail.y + size.y); // <-- size.y is negative here so we are subtracting + + return size; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 mx = (window->DC.CurrentColumns || g.CurrentTable) ? window->WorkRect.Max : window->ContentRegionRect.Max; + return mx - window->DC.CursorPos; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// You should never need those functions. Always use GetCursorScreenPos() and GetContentRegionAvail()! +// They are bizarre local-coordinates which don't play well with scrolling. +ImVec2 ImGui::GetContentRegionMax() +{ + return GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos(); +} + +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Min - window->Pos; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Max - window->Pos; +} +#endif + +// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) +// Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. +// FIXME-OPT: Could we safely early out on ->SkipItems? +void ImGui::BeginGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.GroupStack.resize(g.GroupStack.Size + 1); + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.WindowID = window->ID; + group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; + group_data.BackupIndent = window->DC.Indent; + group_data.BackupGroupOffset = window->DC.GroupOffset; + group_data.BackupCurrLineSize = window->DC.CurrLineSize; + group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; + group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; + group_data.BackupHoveredIdIsAlive = g.HoveredId != 0; + group_data.BackupIsSameLine = window->DC.IsSameLine; + group_data.BackupActiveIdPreviousFrameIsAlive = g.ActiveIdPreviousFrameIsAlive; + group_data.EmitItem = true; + + window->DC.GroupOffset.x = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffset.x; + window->DC.Indent = window->DC.GroupOffset; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.GroupStack.Size > 0); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = g.GroupStack.back(); + IM_ASSERT(group_data.WindowID == window->ID); // EndGroup() in wrong window? + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Include LastItemData.Rect.Max as a workaround for e.g. EndTable() undershooting with CursorMaxPos report. (#7543) + ImRect group_bb(group_data.BackupCursorPos, ImMax(ImMax(window->DC.CursorMaxPos, g.LastItemData.Rect.Max), group_data.BackupCursorPos)); + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorPosPrevLine = group_data.BackupCursorPosPrevLine; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, group_bb.Max); + window->DC.Indent = group_data.BackupIndent; + window->DC.GroupOffset = group_data.BackupGroupOffset; + window->DC.CurrLineSize = group_data.BackupCurrLineSize; + window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; + window->DC.IsSameLine = group_data.BackupIsSameLine; + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return + + if (!group_data.EmitItem) + { + g.GroupStack.pop_back(); + return; + } + + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize()); + ItemAdd(group_bb, 0, NULL, ImGuiItemFlags_NoTabStop); + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group. + // It would be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets. + // Also if you grep for LastItemId you'll notice it is only used in that context. + // (The two tests not the same because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.) + const bool group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive != g.ActiveId) && (g.ActiveIdIsAlive == g.ActiveId) && g.ActiveId; + const bool group_contains_prev_active_id = (group_data.BackupActiveIdPreviousFrameIsAlive == false) && (g.ActiveIdPreviousFrameIsAlive == true); + if (group_contains_curr_active_id) + g.LastItemData.ID = g.ActiveId; + else if (group_contains_prev_active_id) + g.LastItemData.ID = g.ActiveIdPreviousFrame; + g.LastItemData.Rect = group_bb; + + // Forward Hovered flag + const bool group_contains_curr_hovered_id = (group_data.BackupHoveredIdIsAlive == false) && g.HoveredId != 0; + if (group_contains_curr_hovered_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + // Forward Edited flag + if (group_contains_curr_active_id && g.ActiveIdHasBeenEditedThisFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; + + // Forward Deactivated flag + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated; + if (group_contains_prev_active_id && g.ActiveId != g.ActiveIdPreviousFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Deactivated; + + g.GroupStack.pop_back(); + if (g.DebugShowGroupRects) + window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + + +//----------------------------------------------------------------------------- +// [SECTION] SCROLLING +//----------------------------------------------------------------------------- + +// Helper to snap on edges when aiming at an item very close to the edge, +// So the difference between WindowPadding and ItemSpacing will be in the visible area after scrolling. +// When we refactor the scrolling API this may be configurable with a flag? +// Note that the effect for this won't be visible on X axis with default Style settings as WindowPadding.x == ItemSpacing.x by default. +static float CalcScrollEdgeSnap(float target, float snap_min, float snap_max, float snap_threshold, float center_ratio) +{ + if (target <= snap_min + snap_threshold) + return ImLerp(snap_min, target, center_ratio); + if (target >= snap_max - snap_threshold) + return ImLerp(target, snap_max, center_ratio); + return target; +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) +{ + ImVec2 scroll = window->Scroll; + ImVec2 decoration_size(window->DecoOuterSizeX1 + window->DecoInnerSizeX1 + window->DecoOuterSizeX2, window->DecoOuterSizeY1 + window->DecoInnerSizeY1 + window->DecoOuterSizeY2); + for (int axis = 0; axis < 2; axis++) + { + if (window->ScrollTarget[axis] < FLT_MAX) + { + float center_ratio = window->ScrollTargetCenterRatio[axis]; + float scroll_target = window->ScrollTarget[axis]; + if (window->ScrollTargetEdgeSnapDist[axis] > 0.0f) + { + float snap_min = 0.0f; + float snap_max = window->ScrollMax[axis] + window->SizeFull[axis] - decoration_size[axis]; + scroll_target = CalcScrollEdgeSnap(scroll_target, snap_min, snap_max, window->ScrollTargetEdgeSnapDist[axis], center_ratio); + } + scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]); + } + scroll[axis] = IM_ROUND(ImMax(scroll[axis], 0.0f)); + if (!window->Collapsed && !window->SkipItems) + scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]); + } + return scroll; +} + +void ImGui::ScrollToItem(ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ScrollToRectEx(window, g.LastItemData.NavRect, flags); +} + +void ImGui::ScrollToRect(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ScrollToRectEx(window, item_rect, flags); +} + +// Scroll to keep newly navigated item fully into view +ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImRect scroll_rect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1)); + scroll_rect.Min.x = ImMin(scroll_rect.Min.x + window->DecoInnerSizeX1, scroll_rect.Max.x); + scroll_rect.Min.y = ImMin(scroll_rect.Min.y + window->DecoInnerSizeY1, scroll_rect.Max.y); + //GetForegroundDrawList(window)->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255,0,0,255), 0.0f, 0, 5.0f); // [DEBUG] + //GetForegroundDrawList(window)->AddRect(scroll_rect.Min, scroll_rect.Max, IM_COL32_WHITE); // [DEBUG] + + // Check that only one behavior is selected per axis + IM_ASSERT((flags & ImGuiScrollFlags_MaskX_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskX_)); + IM_ASSERT((flags & ImGuiScrollFlags_MaskY_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskY_)); + + // Defaults + ImGuiScrollFlags in_flags = flags; + if ((flags & ImGuiScrollFlags_MaskX_) == 0 && window->ScrollbarX) + flags |= ImGuiScrollFlags_KeepVisibleEdgeX; + if ((flags & ImGuiScrollFlags_MaskY_) == 0) + flags |= window->Appearing ? ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeY; + + const bool fully_visible_x = item_rect.Min.x >= scroll_rect.Min.x && item_rect.Max.x <= scroll_rect.Max.x; + const bool fully_visible_y = item_rect.Min.y >= scroll_rect.Min.y && item_rect.Max.y <= scroll_rect.Max.y; + const bool can_be_fully_visible_x = (item_rect.GetWidth() + g.Style.ItemSpacing.x * 2.0f) <= scroll_rect.GetWidth() || (window->AutoFitFramesX > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + const bool can_be_fully_visible_y = (item_rect.GetHeight() + g.Style.ItemSpacing.y * 2.0f) <= scroll_rect.GetHeight() || (window->AutoFitFramesY > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeX) && !fully_visible_x) + { + if (item_rect.Min.x < scroll_rect.Min.x || !can_be_fully_visible_x) + SetScrollFromPosX(window, item_rect.Min.x - g.Style.ItemSpacing.x - window->Pos.x, 0.0f); + else if (item_rect.Max.x >= scroll_rect.Max.x) + SetScrollFromPosX(window, item_rect.Max.x + g.Style.ItemSpacing.x - window->Pos.x, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterX) && !fully_visible_x) || (flags & ImGuiScrollFlags_AlwaysCenterX)) + { + if (can_be_fully_visible_x) + SetScrollFromPosX(window, ImTrunc((item_rect.Min.x + item_rect.Max.x) * 0.5f) - window->Pos.x, 0.5f); + else + SetScrollFromPosX(window, item_rect.Min.x - window->Pos.x, 0.0f); + } + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeY) && !fully_visible_y) + { + if (item_rect.Min.y < scroll_rect.Min.y || !can_be_fully_visible_y) + SetScrollFromPosY(window, item_rect.Min.y - g.Style.ItemSpacing.y - window->Pos.y, 0.0f); + else if (item_rect.Max.y >= scroll_rect.Max.y) + SetScrollFromPosY(window, item_rect.Max.y + g.Style.ItemSpacing.y - window->Pos.y, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterY) && !fully_visible_y) || (flags & ImGuiScrollFlags_AlwaysCenterY)) + { + if (can_be_fully_visible_y) + SetScrollFromPosY(window, ImTrunc((item_rect.Min.y + item_rect.Max.y) * 0.5f) - window->Pos.y, 0.5f); + else + SetScrollFromPosY(window, item_rect.Min.y - window->Pos.y, 0.0f); + } + + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ImVec2 delta_scroll = next_scroll - window->Scroll; + + // Also scroll parent window to keep us into view if necessary + if (!(flags & ImGuiScrollFlags_NoScrollParent) && (window->Flags & ImGuiWindowFlags_ChildWindow)) + { + // FIXME-SCROLL: May be an option? + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterX | ImGuiScrollFlags_KeepVisibleCenterX)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskX_) | ImGuiScrollFlags_KeepVisibleEdgeX; + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterY | ImGuiScrollFlags_KeepVisibleCenterY)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskY_) | ImGuiScrollFlags_KeepVisibleEdgeY; + delta_scroll += ScrollToRectEx(window->ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll), in_flags); + } + + return delta_scroll; +} + +float ImGui::GetScrollX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.x; +} + +float ImGui::GetScrollY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.y; +} + +float ImGui::GetScrollMaxX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.x; +} + +float ImGui::GetScrollMaxY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.y; +} + +void ImGui::SetScrollX(ImGuiWindow* window, float scroll_x) +{ + window->ScrollTarget.x = scroll_x; + window->ScrollTargetCenterRatio.x = 0.0f; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollY(ImGuiWindow* window, float scroll_y) +{ + window->ScrollTarget.y = scroll_y; + window->ScrollTargetCenterRatio.y = 0.0f; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollX(float scroll_x) +{ + ImGuiContext& g = *GImGui; + SetScrollX(g.CurrentWindow, scroll_x); +} + +void ImGui::SetScrollY(float scroll_y) +{ + ImGuiContext& g = *GImGui; + SetScrollY(g.CurrentWindow, scroll_y); +} + +// Note that a local position will vary depending on initial scroll value, +// This is a little bit confusing so bear with us: +// - local_pos = (absolution_pos - window->Pos) +// - So local_x/local_y are 0.0f for a position at the upper-left corner of a window, +// and generally local_x/local_y are >(padding+decoration) && <(size-padding-decoration) when in the visible area. +// - They mostly exist because of legacy API. +// Following the rules above, when trying to work with scrolling code, consider that: +// - SetScrollFromPosY(0.0f) == SetScrollY(0.0f + scroll.y) == has no effect! +// - SetScrollFromPosY(-scroll.y) == SetScrollY(-scroll.y + scroll.y) == SetScrollY(0.0f) == reset scroll. Of course writing SetScrollY(0.0f) directly then makes more sense +// We store a target position so centering and clamping can occur on the next frame when we are guaranteed to have a known window size +void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio) +{ + IM_ASSERT(center_x_ratio >= 0.0f && center_x_ratio <= 1.0f); + window->ScrollTarget.x = IM_TRUNC(local_x - window->DecoOuterSizeX1 - window->DecoInnerSizeX1 + window->Scroll.x); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.x = center_x_ratio; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio) +{ + IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); + window->ScrollTarget.y = IM_TRUNC(local_y - window->DecoOuterSizeY1 - window->DecoInnerSizeY1 + window->Scroll.y); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.y = center_y_ratio; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollFromPosX(float local_x, float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosX(g.CurrentWindow, local_x, center_x_ratio); +} + +void ImGui::SetScrollFromPosY(float local_y, float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosY(g.CurrentWindow, local_y, center_y_ratio); +} + +// center_x_ratio: 0.0f left of last item, 0.5f horizontal center of last item, 1.0f right of last item. +void ImGui::SetScrollHereX(float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_x = ImMax(window->WindowPadding.x, g.Style.ItemSpacing.x); + float target_pos_x = ImLerp(g.LastItemData.Rect.Min.x - spacing_x, g.LastItemData.Rect.Max.x + spacing_x, center_x_ratio); + SetScrollFromPosX(window, target_pos_x - window->Pos.x, center_x_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.x = ImMax(0.0f, window->WindowPadding.x - spacing_x); +} + +// center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. +void ImGui::SetScrollHereY(float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_y = ImMax(window->WindowPadding.y, g.Style.ItemSpacing.y); + float target_pos_y = ImLerp(window->DC.CursorPosPrevLine.y - spacing_y, window->DC.CursorPosPrevLine.y + window->DC.PrevLineSize.y + spacing_y, center_y_ratio); + SetScrollFromPosY(window, target_pos_y - window->Pos.y, center_y_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.y = ImMax(0.0f, window->WindowPadding.y - spacing_y); +} + +//----------------------------------------------------------------------------- +// [SECTION] TOOLTIPS +//----------------------------------------------------------------------------- + +bool ImGui::BeginTooltip() +{ + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +bool ImGui::BeginItemTooltip() +{ + if (!IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + return false; + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +bool ImGui::BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + + if (g.DragDropWithinSource || g.DragDropWithinTarget) + { + // Drag and Drop tooltips are positioning differently than other tooltips: + // - offset visibility to increase visibility around mouse. + // - never clamp within outer viewport boundary. + // We call SetNextWindowPos() to enforce position and disable clamping. + // See FindBestWindowPosForPopup() for positionning logic of other tooltips (not drag and drop ones). + //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; + ImVec2 tooltip_pos = g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET * g.Style.MouseCursorScale; + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) == 0) + SetNextWindowPos(tooltip_pos); + SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); + //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( + tooltip_flags |= ImGuiTooltipFlags_OverridePrevious; + } + + char window_name[16]; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); + if (tooltip_flags & ImGuiTooltipFlags_OverridePrevious) + if (ImGuiWindow* window = FindWindowByName(window_name)) + if (window->Active) + { + // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. + SetWindowHiddenAndSkipItemsForCurrentFrame(window); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); + } + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize; + Begin(window_name, NULL, flags | extra_window_flags); + // 2023-03-09: Added bool return value to the API, but currently always returning true. + // If this ever returns false we need to update BeginDragDropSource() accordingly. + //if (!ret) + // End(); + //return ret; + return true; +} + +void ImGui::EndTooltip() +{ + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); +} + +void ImGui::SetTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; + TextV(fmt, args); + EndTooltip(); +} + +// Shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav'. +// Defaults to == ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort when using the mouse. +void ImGui::SetItemTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetItemTooltipV(const char* fmt, va_list args) +{ + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); +} + + +//----------------------------------------------------------------------------- +// [SECTION] POPUPS +//----------------------------------------------------------------------------- + +// Supported flags: ImGuiPopupFlags_AnyPopupId, ImGuiPopupFlags_AnyPopupLevel +bool ImGui::IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + if (popup_flags & ImGuiPopupFlags_AnyPopupId) + { + // Return true if any popup is open at the current BeginPopup() level of the popup stack + // This may be used to e.g. test for another popups already opened to handle popups priorities at the same level. + IM_ASSERT(id == 0); + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + return g.OpenPopupStack.Size > 0; + else + return g.OpenPopupStack.Size > g.BeginPopupStack.Size; + } + else + { + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + { + // Return true if the popup is open anywhere in the popup stack + for (int n = 0; n < g.OpenPopupStack.Size; n++) + if (g.OpenPopupStack[n].PopupId == id) + return true; + return false; + } + else + { + // Return true if the popup is open at the current BeginPopup() level of the popup stack (this is the most-common query) + return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == id; + } + } +} + +bool ImGui::IsPopupOpen(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = (popup_flags & ImGuiPopupFlags_AnyPopupId) ? 0 : g.CurrentWindow->GetID(str_id); + if ((popup_flags & ImGuiPopupFlags_AnyPopupLevel) && id != 0) + IM_ASSERT(0 && "Cannot use IsPopupOpen() with a string id and ImGuiPopupFlags_AnyPopupLevel."); // But non-string version is legal and used internally + return IsPopupOpen(id, popup_flags); +} + +// Also see FindBlockingModal(NULL) +ImGuiWindow* ImGui::GetTopMostPopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if (popup->Flags & ImGuiWindowFlags_Modal) + return popup; + return NULL; +} + +// See Demo->Stacked Modal to confirm what this is for. +ImGuiWindow* ImGui::GetTopMostAndVisiblePopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if ((popup->Flags & ImGuiWindowFlags_Modal) && IsWindowActiveAndVisible(popup)) + return popup; + return NULL; +} + +void ImGui::OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.CurrentWindow->GetID(str_id); + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopup(\"%s\" -> 0x%08X)\n", str_id, id); + OpenPopupEx(id, popup_flags); +} + +void ImGui::OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + OpenPopupEx(id, popup_flags); +} + +// Mark popup as open (toggle toward open state). +// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. +// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). +// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) +void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + const int current_stack_size = g.BeginPopupStack.Size; + + if (popup_flags & ImGuiPopupFlags_NoOpenOverExistingPopup) + if (IsPopupOpen((ImGuiID)0, ImGuiPopupFlags_AnyPopupId)) + return; + + ImGuiPopupData popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. + popup_ref.PopupId = id; + popup_ref.Window = NULL; + popup_ref.RestoreNavWindow = g.NavWindow; // When popup closes focus may be restored to NavWindow (depend on window type). + popup_ref.OpenFrameCount = g.FrameCount; + popup_ref.OpenParentId = parent_window->IDStack.back(); + popup_ref.OpenPopupPos = NavCalcPreferredRefPos(); + popup_ref.OpenMousePos = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : popup_ref.OpenPopupPos; + + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopupEx(0x%08X)\n", id); + if (g.OpenPopupStack.Size < current_stack_size + 1) + { + g.OpenPopupStack.push_back(popup_ref); + } + else + { + // Gently handle the user mistakenly calling OpenPopup() every frames: it is likely a programming mistake! + // However, if we were to run the regular code path, the ui would become completely unusable because the popup will always be + // in hidden-while-calculating-size state _while_ claiming focus. Which is extremely confusing situation for the programmer. + // Instead, for successive frames calls to OpenPopup(), we silently avoid reopening even if ImGuiPopupFlags_NoReopen is not specified. + bool keep_existing = false; + if (g.OpenPopupStack[current_stack_size].PopupId == id) + if ((g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) || (popup_flags & ImGuiPopupFlags_NoReopen)) + keep_existing = true; + if (keep_existing) + { + // No reopen + g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; + } + else + { + // Reopen: close child popups if any, then flag popup for open/reopen (set position, focus, init navigation) + ClosePopupToLevel(current_stack_size, true); + g.OpenPopupStack.push_back(popup_ref); + } + + // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). + // This is equivalent to what ClosePopupToLevel() does. + //if (g.OpenPopupStack[current_stack_size].PopupId == id) + // FocusWindow(parent_window); + } +} + +// When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. +// This function closes any popups that are over 'ref_window'. +void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size == 0) + return; + + // Don't close our own child popup windows. + //IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\") restore_under=%d\n", ref_window ? ref_window->Name : "", restore_focus_to_window_under_popup); + int popup_count_to_keep = 0; + if (ref_window) + { + // Find the highest popup which is a descendant of the reference window (generally reference window = NavWindow) + for (; popup_count_to_keep < g.OpenPopupStack.Size; popup_count_to_keep++) + { + ImGuiPopupData& popup = g.OpenPopupStack[popup_count_to_keep]; + if (!popup.Window) + continue; + IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); + + // Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow) + // - Clicking/Focusing Window2 won't close Popup1: + // Window -> Popup1 -> Window2(Ref) + // - Clicking/focusing Popup1 will close Popup2 and Popup3: + // Window -> Popup1(Ref) -> Popup2 -> Popup3 + // - Each popups may contain child windows, which is why we compare ->RootWindow! + // Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child + // We step through every popup from bottom to top to validate their position relative to reference window. + bool ref_window_is_descendent_of_popup = false; + for (int n = popup_count_to_keep; n < g.OpenPopupStack.Size; n++) + if (ImGuiWindow* popup_window = g.OpenPopupStack[n].Window) + if (IsWindowWithinBeginStackOf(ref_window, popup_window)) + { + ref_window_is_descendent_of_popup = true; + break; + } + if (!ref_window_is_descendent_of_popup) + break; + } + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + { + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\")\n", ref_window ? ref_window->Name : ""); + ClosePopupToLevel(popup_count_to_keep, restore_focus_to_window_under_popup); + } +} + +void ImGui::ClosePopupsExceptModals() +{ + ImGuiContext& g = *GImGui; + + int popup_count_to_keep; + for (popup_count_to_keep = g.OpenPopupStack.Size; popup_count_to_keep > 0; popup_count_to_keep--) + { + ImGuiWindow* window = g.OpenPopupStack[popup_count_to_keep - 1].Window; + if (!window || (window->Flags & ImGuiWindowFlags_Modal)) + break; + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + ClosePopupToLevel(popup_count_to_keep, true); +} + +void ImGui::ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_under=%d\n", remaining, restore_focus_to_window_under_popup); + IM_ASSERT(remaining >= 0 && remaining < g.OpenPopupStack.Size); + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) + for (int n = remaining; n < g.OpenPopupStack.Size; n++) + IMGUI_DEBUG_LOG_POPUP("[popup] - Closing PopupID 0x%08X Window \"%s\"\n", g.OpenPopupStack[n].PopupId, g.OpenPopupStack[n].Window ? g.OpenPopupStack[n].Window->Name : NULL); + + // Trim open popup stack + ImGuiPopupData prev_popup = g.OpenPopupStack[remaining]; + g.OpenPopupStack.resize(remaining); + + // Restore focus (unless popup window was not yet submitted, and didn't have a chance to take focus anyhow. See #7325 for an edge case) + if (restore_focus_to_window_under_popup && prev_popup.Window) + { + ImGuiWindow* popup_window = prev_popup.Window; + ImGuiWindow* focus_window = (popup_window->Flags & ImGuiWindowFlags_ChildMenu) ? popup_window->ParentWindow : prev_popup.RestoreNavWindow; + if (focus_window && !focus_window->WasActive) + FocusTopMostWindowUnderOne(popup_window, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); // Fallback + else + FocusWindow(focus_window, (g.NavLayer == ImGuiNavLayer_Main) ? ImGuiFocusRequestFlags_RestoreFocusedChild : ImGuiFocusRequestFlags_None); + } +} + +// Close the popup we have begin-ed into. +void ImGui::CloseCurrentPopup() +{ + ImGuiContext& g = *GImGui; + int popup_idx = g.BeginPopupStack.Size - 1; + if (popup_idx < 0 || popup_idx >= g.OpenPopupStack.Size || g.BeginPopupStack[popup_idx].PopupId != g.OpenPopupStack[popup_idx].PopupId) + return; + + // Closing a menu closes its top-most parent popup (unless a modal) + while (popup_idx > 0) + { + ImGuiWindow* popup_window = g.OpenPopupStack[popup_idx].Window; + ImGuiWindow* parent_popup_window = g.OpenPopupStack[popup_idx - 1].Window; + bool close_parent = false; + if (popup_window && (popup_window->Flags & ImGuiWindowFlags_ChildMenu)) + if (parent_popup_window && !(parent_popup_window->Flags & ImGuiWindowFlags_MenuBar)) + close_parent = true; + if (!close_parent) + break; + popup_idx--; + } + IMGUI_DEBUG_LOG_POPUP("[popup] CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size - 1, popup_idx); + ClosePopupToLevel(popup_idx, true); + + // A common pattern is to close a popup when selecting a menu item/selectable that will open another window. + // To improve this usage pattern, we avoid nav highlight for a single frame in the parent window. + // Similarly, we could avoid mouse hover highlight in this window but it is less visually problematic. + if (ImGuiWindow* window = g.NavWindow) + window->DC.NavHideHighlightOneFrame = true; +} + +// Attention! BeginPopup() adds default flags when calling BeginPopupEx()! +bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + char name[20]; + if (extra_window_flags & ImGuiWindowFlags_ChildMenu) + ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.BeginMenuDepth); // Recycle windows based on depth + else + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame + + bool is_open = Begin(name, NULL, extra_window_flags | ImGuiWindowFlags_Popup); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; + + return is_open; +} + +bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= g.BeginPopupStack.Size) // Early out for performance + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + flags |= ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings; + ImGuiID id = g.CurrentWindow->GetID(str_id); + return BeginPopupEx(id, flags); +} + +// If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup. +// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup). +// - *p_open set back to false in BeginPopupModal() when popup is not open. +// - if you set *p_open to false before calling BeginPopupModal(), it will close the popup. +bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = window->GetID(name); + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (p_open && *p_open) + *p_open = false; + return false; + } + + // Center modal windows by default for increased visibility + // (this won't really last as settings will kick in, and is mostly for backward compatibility. user may do the same themselves) + // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + } + + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse; + const bool is_open = Begin(name, p_open, flags); + if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + { + EndPopup(); + if (is_open) + ClosePopupToLevel(g.BeginPopupStack.Size, true); + return false; + } + return is_open; +} + +void ImGui::EndPopup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + IM_ASSERT(g.BeginPopupStack.Size > 0); + + // Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests) + if (g.NavWindow == window) + NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY); + + // Child-popups don't need to be laid out + IM_ASSERT(g.WithinEndChild == false); + if (window->Flags & ImGuiWindowFlags_ChildWindow) + g.WithinEndChild = true; + End(); + g.WithinEndChild = false; +} + +// Helper to open a popup if mouse button is released over the item +// - This is essentially the same as BeginPopupContextItem() but without the trailing BeginPopup() +void ImGui::OpenPopupOnItemClick(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + OpenPopupEx(id, popup_flags); + } +} + +// This is a helper to handle the simplest case of associating one named popup to one given widget. +// - To create a popup associated to the last item, you generally want to pass a NULL value to str_id. +// - To create a popup with a specific identifier, pass it in str_id. +// - This is useful when using using BeginPopupContextItem() on an item which doesn't have an identifier, e.g. a Text() call. +// - This is useful when multiple code locations may want to manipulate/open the same popup, given an explicit id. +// - You may want to handle the whole on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). +// This is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// OpenPopupOnItemClick(str_id, ImGuiPopupFlags_MouseButtonRight); +// return BeginPopup(id); +// Which is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) +// OpenPopup(id); +// return BeginPopup(id); +// The main difference being that this is tweaked to avoid computing the ID twice. +bool ImGui::BeginPopupContextItem(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextWindow(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "window_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + if (!(popup_flags & ImGuiPopupFlags_NoOpenOverItems) || !IsAnyItemHovered()) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextVoid(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "void_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) + if (GetTopMostPopupModal() == NULL) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +// r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) +// r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. +// (r_outer is usually equivalent to the viewport rectangle minus padding, but when multi-viewports are enabled and monitor +// information are available, it may represent the entire platform monitor from the frame of reference of the current viewport. +// this allows us to have tooltips/popups displayed out of the parent viewport.) +ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy) +{ + ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); + //GetForegroundDrawList()->AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + + // Combo Box policy (we want a connecting edge) + if (policy == ImGuiPopupPositionPolicy_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Tooltip and Default popup policy + // (Always first try the direction we used on the last frame, if any) + if (policy == ImGuiPopupPositionPolicy_Tooltip || policy == ImGuiPopupPositionPolicy_Default) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + + const float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + const float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + + // If there's not enough room on one axis, there's no point in positioning on a side on this axis (e.g. when not enough width, use a top/bottom position to maximize available width) + if (avail_w < size.x && (dir == ImGuiDir_Left || dir == ImGuiDir_Right)) + continue; + if (avail_h < size.y && (dir == ImGuiDir_Up || dir == ImGuiDir_Down)) + continue; + + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + + // Clamp top-left corner of popup + pos.x = ImMax(pos.x, r_outer.Min.x); + pos.y = ImMax(pos.y, r_outer.Min.y); + + *last_dir = dir; + return pos; + } + } + + // Fallback when not enough room: + *last_dir = ImGuiDir_None; + + // For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + if (policy == ImGuiPopupPositionPolicy_Tooltip) + return ref_pos + ImVec2(2, 2); + + // Otherwise try to keep within display + ImVec2 pos = ref_pos; + pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); + pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); + return pos; +} + +// Note that this is used for popups, which can overlap the non work-area of individual viewports. +ImRect ImGui::GetPopupAllowedExtentRect(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(window); + ImRect r_screen = ((ImGuiViewportP*)(void*)GetMainViewport())->GetMainRect(); + ImVec2 padding = g.Style.DisplaySafeAreaPadding; + r_screen.Expand(ImVec2((r_screen.GetWidth() > padding.x * 2) ? -padding.x : 0.0f, (r_screen.GetHeight() > padding.y * 2) ? -padding.y : 0.0f)); + return r_screen; +} + +ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + ImRect r_outer = GetPopupAllowedExtentRect(window); + if (window->Flags & ImGuiWindowFlags_ChildMenu) + { + // Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds. + // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. + IM_ASSERT(g.CurrentWindow == window); + ImGuiWindow* parent_window = g.CurrentWindowStack[g.CurrentWindowStack.Size - 2].Window; + float horizontal_overlap = g.Style.ItemInnerSpacing.x; // We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x). + ImRect r_avoid; + if (parent_window->DC.MenuBarAppending) + r_avoid = ImRect(-FLT_MAX, parent_window->ClipRect.Min.y, FLT_MAX, parent_window->ClipRect.Max.y); // Avoid parent menu-bar. If we wanted multi-line menu-bar, we may instead want to have the calling window setup e.g. a NextWindowData.PosConstraintAvoidRect field + else + r_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Default); + } + if (window->Flags & ImGuiWindowFlags_Popup) + { + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, ImRect(window->Pos, window->Pos), ImGuiPopupPositionPolicy_Default); // Ideally we'd disable r_avoid here + } + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Position tooltip (always follows mouse + clamp within outer boundaries) + // Note that drag and drop tooltips are NOT using this path: BeginTooltipEx() manually sets their position. + // In theory we could handle both cases in same location, but requires a bit of shuffling as drag and drop tooltips are calling SetWindowPos() leading to 'window_pos_set_by_api' being set in Begin() + IM_ASSERT(g.CurrentWindow == window); + const float scale = g.Style.MouseCursorScale; + const ImVec2 ref_pos = NavCalcPreferredRefPos(); + const ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET * scale; + ImRect r_avoid; + if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos)) + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); + else + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * scale, ref_pos.y + 24 * scale); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255, 0, 255, 255)); + return FindBestWindowPosForPopupEx(tooltip_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); + } + IM_ASSERT(0); + return window->Pos; +} + +//----------------------------------------------------------------------------- +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +//----------------------------------------------------------------------------- + +// FIXME-NAV: The existence of SetNavID vs SetFocusID vs FocusWindow() needs to be clarified/reworked. +// In our terminology those should be interchangeable, yet right now this is super confusing. +// Those two functions are merely a legacy artifact, so at minimum naming should be clarified. + +void ImGui::SetNavWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.NavWindow != window) + { + IMGUI_DEBUG_LOG_FOCUS("[focus] SetNavWindow(\"%s\")\n", window ? window->Name : ""); + g.NavWindow = window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + g.NavInitRequest = g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavHighlightActivated(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavHighlightActivatedId = id; + g.NavHighlightActivatedTimer = NAV_ACTIVATE_HIGHLIGHT_TIMER; +} + +void ImGui::NavClearPreferredPosForAxis(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer][axis] = FLT_MAX; +} + +void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + IM_ASSERT(nav_layer == ImGuiNavLayer_Main || nav_layer == ImGuiNavLayer_Menu); + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(focus_scope_id); + g.NavWindow->NavLastIds[nav_layer] = id; + g.NavWindow->NavRectRel[nav_layer] = rect_rel; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + if (g.NavWindow != window) + SetNavWindow(window); + + // Assume that SetFocusID() is called in the context where its window->DC.NavLayerCurrent and g.CurrentFocusScopeId are valid. + // Note that window may be != g.CurrentWindow (e.g. SetFocusID call in InputTextEx for multi-line text) + const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(g.CurrentFocusScopeId); + window->NavLastIds[nav_layer] = id; + if (g.LastItemData.ID == id) + window->NavRectRel[nav_layer] = WindowRectAbsToRel(window, g.LastItemData.NavRect); + + if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + g.NavDisableMouseHover = true; + else + g.NavDisableHighlight = true; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +static ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy) +{ + if (ImFabs(dx) > ImFabs(dy)) + return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; + return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; +} + +static float inline NavScoreItemDistInterval(float cand_min, float cand_max, float curr_min, float curr_max) +{ + if (cand_max < curr_min) + return cand_max - curr_min; + if (curr_max < cand_min) + return cand_min - curr_max; + return 0.0f; +} + +// Scoring function for gamepad/keyboard directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool ImGui::NavScoreItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavLayer != window->DC.NavLayerCurrent) + return false; + + // FIXME: Those are not good variables names + ImRect cand = g.LastItemData.NavRect; // Current item nav rectangle + const ImRect curr = g.NavScoringRect; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringDebugCount++; + + // When entering through a NavFlattened border, we consider child window items as fully clipped for scoring + if (window->ParentWindow == g.NavWindow) + { + IM_ASSERT((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened); + if (!window->ClipRect.Overlaps(cand)) + return false; + cand.ClipWithFull(window->ClipRect); // This allows the scored item to not overlap other candidates in the parent window + } + + // Compute distance between boxes + // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. + float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); + float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items + if (dby != 0.0f && dbx != 0.0f) + dbx = (dbx / 1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + float dist_box = ImFabs(dbx) + ImFabs(dby); + + // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) + float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); + float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); + float dist_center = ImFabs(dcx) + ImFabs(dcy); // L1 metric (need this for our connectedness guarantee) + + // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance + ImGuiDir quadrant; + float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; + if (dbx != 0.0f || dby != 0.0f) + { + // For non-overlapping boxes, use distance between boxes + // FIXME-NAV: Quadrant may be incorrect because of (1) dbx bias and (2) curr.Max.y bias applied by NavBiasScoringRect() where typically curr.Max.y==curr.Min.y + // One typical case where this happens, with style.WindowMenuButtonPosition == ImGuiDir_Right, pressing Left to navigate from Close to Collapse tends to fail. + // Also see #6344. Calling ImGetDirQuadrantFromDelta() with unbiased values may be good but side-effects are plenty. + dax = dbx; + day = dby; + dist_axial = dist_box; + quadrant = ImGetDirQuadrantFromDelta(dbx, dby); + } + else if (dcx != 0.0f || dcy != 0.0f) + { + // For overlapping boxes with different centers, use distance between centers + dax = dcx; + day = dcy; + dist_axial = dist_center; + quadrant = ImGetDirQuadrantFromDelta(dcx, dcy); + } + else + { + // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) + quadrant = (g.LastItemData.ID < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + } + + const ImGuiDir move_dir = g.NavMoveDir; +#if IMGUI_DEBUG_NAV_SCORING + char buf[200]; + if (g.IO.KeyCtrl) // Hold CTRL to preview score in matching quadrant. CTRL+Arrow to rotate. + { + if (quadrant == move_dir) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 80)); + draw_list->AddRectFilled(cand.Min, cand.Min + CalcTextSize(buf), IM_COL32(255, 0, 0, 200)); + draw_list->AddText(cand.Min, IM_COL32(255, 255, 255, 255), buf); + } + } + const bool debug_hovering = IsMouseHoveringRect(cand.Min, cand.Max); + const bool debug_tty = (g.IO.KeyCtrl && IsKeyPressed(ImGuiKey_Space)); + if (debug_hovering || debug_tty) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), + "d-box (%7.3f,%7.3f) -> %7.3f\nd-center (%7.3f,%7.3f) -> %7.3f\nd-axial (%7.3f,%7.3f) -> %7.3f\nnav %c, quadrant %c", + dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "-WENS"[move_dir+1], "-WENS"[quadrant+1]); + if (debug_hovering) + { + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100)); + draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255, 255, 0, 200)); + draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40, 0, 0, 200)); + draw_list->AddText(cand.Max, ~0U, buf); + } + if (debug_tty) { IMGUI_DEBUG_LOG_NAV("id 0x%08X\n%s\n", g.LastItemData.ID, buf); } + } +#endif + + // Is it in the quadrant we're interested in moving to? + bool new_best = false; + if (quadrant == move_dir) + { + // Does it beat the current best candidate? + if (dist_box < result->DistBox) + { + result->DistBox = dist_box; + result->DistCenter = dist_center; + return true; + } + if (dist_box == result->DistBox) + { + // Try using distance between center points to break ties + if (dist_center < result->DistCenter) + { + result->DistCenter = dist_center; + new_best = true; + } + else if (dist_center == result->DistCenter) + { + // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items + // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), + // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. + if (((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance + new_best = true; + } + } + } + + // Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches + // are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness) + // This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too. + // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. + // Disabling it may lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? + if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match + if (g.NavLayer == ImGuiNavLayer_Menu && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + if ((move_dir == ImGuiDir_Left && dax < 0.0f) || (move_dir == ImGuiDir_Right && dax > 0.0f) || (move_dir == ImGuiDir_Up && day < 0.0f) || (move_dir == ImGuiDir_Down && day > 0.0f)) + { + result->DistAxial = dist_axial; + new_best = true; + } + + return new_best; +} + +static void ImGui::NavApplyItemToResult(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + result->Window = window; + result->ID = g.LastItemData.ID; + result->FocusScopeId = g.CurrentFocusScopeId; + result->InFlags = g.LastItemData.InFlags; + result->RectRel = WindowRectAbsToRel(window, g.LastItemData.NavRect); + if (result->InFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + result->SelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } +} + +// True when current work location may be scrolled horizontally when moving left / right. +// This is generally always true UNLESS within a column. We don't have a vertical equivalent. +void ImGui::NavUpdateCurrentWindowIsScrollPushableX() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.NavIsScrollPushableX = (g.CurrentTable == NULL && window->DC.CurrentColumns == NULL); +} + +// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) +// This is called after LastItemData is set, but NextItemData is also still valid. +static void ImGui::NavProcessItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = g.LastItemData.ID; + const ImGuiItemFlags item_flags = g.LastItemData.InFlags; + + // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221) + if (window->DC.NavIsScrollPushableX == false) + { + g.LastItemData.NavRect.Min.x = ImClamp(g.LastItemData.NavRect.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + g.LastItemData.NavRect.Max.x = ImClamp(g.LastItemData.NavRect.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + } + const ImRect nav_bb = g.LastItemData.NavRect; + + // Process Init Request + if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent && (item_flags & ImGuiItemFlags_Disabled) == 0) + { + // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback + const bool candidate_for_nav_default_focus = (item_flags & ImGuiItemFlags_NoNavDefaultFocus) == 0; + if (candidate_for_nav_default_focus || g.NavInitResult.ID == 0) + { + NavApplyItemToResult(&g.NavInitResult); + } + if (candidate_for_nav_default_focus) + { + g.NavInitRequest = false; // Found a match, clear request + NavUpdateAnyRequestFlag(); + } + } + + // Process Move Request (scoring for navigation) + // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy) + if (g.NavMoveScoringItems && (item_flags & ImGuiItemFlags_Disabled) == 0) + { + if ((g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi) || (window->Flags & ImGuiWindowFlags_NoNavInputs) == 0) + { + const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + if (is_tabbing) + { + NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags); + } + else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) + { + ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (NavScoreItem(result)) + NavApplyItemToResult(result); + + // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. + const float VISIBLE_RATIO = 0.70f; + if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) + if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) + if (NavScoreItem(&g.NavMoveResultLocalVisible)) + NavApplyItemToResult(&g.NavMoveResultLocalVisible); + } + } + } + + // Update information for currently focused/navigated item + if (g.NavId == id) + { + if (g.NavWindow != window) + SetNavWindow(window); // Always refresh g.NavWindow, because some operations such as FocusItem() may not have a window. + g.NavLayer = window->DC.NavLayerCurrent; + SetNavFocusScope(g.CurrentFocusScopeId); // Will set g.NavFocusScopeId AND store g.NavFocusScopePath + g.NavFocusScopeId = g.CurrentFocusScopeId; + g.NavIdIsAlive = true; + if (g.LastItemData.InFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + g.NavLastValidSelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } + window->NavRectRel[window->DC.NavLayerCurrent] = WindowRectAbsToRel(window, nav_bb); // Store item bounding box (relative to window position) + } +} + +// Handle "scoring" of an item for a tabbing/focusing request initiated by NavUpdateCreateTabbingRequest(). +// Note that SetKeyboardFocusHere() API calls are considered tabbing requests! +// - Case 1: no nav/active id: set result to first eligible item, stop storing. +// - Case 2: tab forward: on ref id set counter, on counter elapse store result +// - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. // FIXME-TABBING: Could be done as a next-frame forwarded request +// - Case 4: tab backward: store all results, on ref id pick prev, stop storing +// - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last // FIXME-TABBING: Could be done as next-frame forwarded requested +void ImGui::NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags) +{ + ImGuiContext& g = *GImGui; + + if ((move_flags & ImGuiNavMoveFlags_FocusApi) == 0) + { + if (g.NavLayer != g.CurrentWindow->DC.NavLayerCurrent) + return; + if (g.NavFocusScopeId != g.CurrentFocusScopeId) + return; + } + + // - Can always land on an item when using API call. + // - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item. + // - Tabbing without _NavEnableKeyboard: goes through inputable items only. + bool can_stop; + if (move_flags & ImGuiNavMoveFlags_FocusApi) + can_stop = true; + else + can_stop = (item_flags & ImGuiItemFlags_NoTabStop) == 0 && ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) || (item_flags & ImGuiItemFlags_Inputable)); + + // Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows) + ImGuiNavItemData* result = &g.NavMoveResultLocal; + if (g.NavTabbingDir == +1) + { + // Tab Forward or SetKeyboardFocusHere() with >= 0 + if (can_stop && g.NavTabbingResultFirst.ID == 0) + NavApplyItemToResult(&g.NavTabbingResultFirst); + if (can_stop && g.NavTabbingCounter > 0 && --g.NavTabbingCounter == 0) + NavMoveRequestResolveWithLastItem(result); + else if (g.NavId == id) + g.NavTabbingCounter = 1; + } + else if (g.NavTabbingDir == -1) + { + // Tab Backward + if (g.NavId == id) + { + if (result->ID) + { + g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); + } + } + else if (can_stop) + { + // Keep applying until reaching NavId + NavApplyItemToResult(result); + } + } + else if (g.NavTabbingDir == 0) + { + if (can_stop && g.NavId == id) + NavMoveRequestResolveWithLastItem(result); + if (can_stop && g.NavTabbingResultFirst.ID == 0) // Tab init + NavApplyItemToResult(&g.NavTabbingResultFirst); + } +} + +bool ImGui::NavMoveRequestButNoResultYet() +{ + ImGuiContext& g = *GImGui; + return g.NavMoveScoringItems && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +// FIXME: ScoringRect is not set +void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + //IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestSubmit: dir %c, window \"%s\"\n", "-WENS"[move_dir + 1], g.NavWindow->Name); + + if (move_flags & ImGuiNavMoveFlags_IsTabbing) + move_flags |= ImGuiNavMoveFlags_AllowCurrentNavId; + + g.NavMoveSubmitted = g.NavMoveScoringItems = true; + g.NavMoveDir = move_dir; + g.NavMoveDirForDebug = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags; + g.NavMoveScrollFlags = scroll_flags; + g.NavMoveForwardToNextFrame = false; + g.NavMoveKeyMods = (move_flags & ImGuiNavMoveFlags_FocusApi) ? 0 : g.IO.KeyMods; + g.NavMoveResultLocal.Clear(); + g.NavMoveResultLocalVisible.Clear(); + g.NavMoveResultOther.Clear(); + g.NavTabbingCounter = 0; + g.NavTabbingResultFirst.Clear(); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; // Ensure request doesn't need more processing + NavApplyItemToResult(result); + NavUpdateAnyRequestFlag(); +} + +// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere +void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; + g.LastItemData.ID = tree_node_data->ID; + g.LastItemData.InFlags = tree_node_data->InFlags & ~ImGuiItemFlags_HasSelectionUserData; // Losing SelectionUserData, recovered next-frame (cheaper). + g.LastItemData.NavRect = tree_node_data->NavRect; + NavApplyItemToResult(result); // Result this instead of implementing a NavApplyPastTreeNodeToResult() + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestCancel() +{ + ImGuiContext& g = *GImGui; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +// Forward will reuse the move request again on the next frame (generally with modifications done to it) +void ImGui::NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavMoveForwardToNextFrame == false); + NavMoveRequestCancel(); + g.NavMoveForwardToNextFrame = true; + g.NavMoveDir = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags | ImGuiNavMoveFlags_Forwarded; + g.NavMoveScrollFlags = scroll_flags; +} + +// Navigation wrap-around logic is delayed to the end of the frame because this operation is only valid after entire +// popup is assembled and in case of appended popups it is not clear which EndPopup() call is final. +void ImGui::NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags wrap_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT((wrap_flags & ImGuiNavMoveFlags_WrapMask_ ) != 0 && (wrap_flags & ~ImGuiNavMoveFlags_WrapMask_) == 0); // Call with _WrapX, _WrapY, _LoopX, _LoopY + + // In theory we should test for NavMoveRequestButNoResultYet() but there's no point doing it: + // as NavEndFrame() will do the same test. It will end up calling NavUpdateCreateWrappingRequest(). + if (g.NavWindow == window && g.NavMoveScoringItems && g.NavLayer == ImGuiNavLayer_Main) + g.NavMoveFlags = (g.NavMoveFlags & ~ImGuiNavMoveFlags_WrapMask_) | wrap_flags; +} + +// FIXME: This could be replaced by updating a frame number in each window when (window == NavWindow) and (NavLayer == 0). +// This way we could find the last focused window among our children. It would be much less confusing this way? +static void ImGui::NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window) +{ + ImGuiWindow* parent = nav_window; + while (parent && parent->RootWindow != parent && (parent->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent = parent->ParentWindow; + if (parent && parent != nav_window) + parent->NavLastChildNavWindow = nav_window; +} + +// Restore the last focused child. +// Call when we are expected to land on the Main Layer (0) after FocusWindow() +static ImGuiWindow* ImGui::NavRestoreLastChildNavWindow(ImGuiWindow* window) +{ + if (window->NavLastChildNavWindow && window->NavLastChildNavWindow->WasActive) + return window->NavLastChildNavWindow; + return window; +} + +void ImGui::NavRestoreLayer(ImGuiNavLayer layer) +{ + ImGuiContext& g = *GImGui; + if (layer == ImGuiNavLayer_Main) + { + ImGuiWindow* prev_nav_window = g.NavWindow; + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); // FIXME-NAV: Should clear ongoing nav requests? + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + if (prev_nav_window) + IMGUI_DEBUG_LOG_FOCUS("[focus] NavRestoreLayer: from \"%s\" to SetNavWindow(\"%s\")\n", prev_nav_window->Name, g.NavWindow->Name); + } + ImGuiWindow* window = g.NavWindow; + if (window->NavLastIds[layer] != 0) + { + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + } + else + { + g.NavLayer = layer; + NavInitWindow(window, true); + } +} + +void ImGui::NavRestoreHighlightAfterMove() +{ + ImGuiContext& g = *GImGui; + g.NavDisableHighlight = false; + g.NavDisableMouseHover = g.NavMousePosDirty = true; +} + +static inline void ImGui::NavUpdateAnyRequestFlag() +{ + ImGuiContext& g = *GImGui; + g.NavAnyRequest = g.NavMoveScoringItems || g.NavInitRequest || (IMGUI_DEBUG_NAV_SCORING && g.NavWindow != NULL); + if (g.NavAnyRequest) + IM_ASSERT(g.NavWindow != NULL); +} + +// This needs to be called before we submit any widget (aka in or before Begin) +void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == g.NavWindow); + + if (window->Flags & ImGuiWindowFlags_NoNavInputs) + { + g.NavId = 0; + SetNavFocusScope(window->NavRootFocusScopeId); + return; + } + + bool init_for_nav = false; + if (window == window->RootWindow || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) + init_for_nav = true; + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from NavInitWindow(), init_for_nav=%d, window=\"%s\", layer=%d\n", init_for_nav, window->Name, g.NavLayer); + if (init_for_nav) + { + SetNavID(0, g.NavLayer, window->NavRootFocusScopeId, ImRect()); + g.NavInitRequest = true; + g.NavInitRequestFromMove = false; + g.NavInitResult.ID = 0; + NavUpdateAnyRequestFlag(); + } + else + { + g.NavId = window->NavLastIds[0]; + SetNavFocusScope(window->NavRootFocusScopeId); + } +} + +static ImVec2 ImGui::NavCalcPreferredRefPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID; + + // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag. + if ((g.NavDisableHighlight || !g.NavDisableMouseHover || !window) && !activated_shortcut) + { + // Mouse (we need a fallback in case the mouse becomes invalid after being used) + // The +1.0f offset when stored by OpenPopupEx() allows reopening this or another popup (same or another mouse button) while not moving the mouse, it is pretty standard. + // In theory we could move that +1.0f offset in OpenPopupEx() + ImVec2 p = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : g.MouseLastValidPos; + return ImVec2(p.x + 1.0f, p.y); + } + else + { + // When navigation is active and mouse is disabled, pick a position around the bottom left of the currently navigated item + ImRect ref_rect; + if (activated_shortcut) + ref_rect = g.LastItemData.NavRect; + else + ref_rect = WindowRectRelToAbs(window, window->NavRectRel[g.NavLayer]); + + // Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?) + if (window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX)) + { + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ref_rect.Translate(window->Scroll - next_scroll); + } + ImVec2 pos = ImVec2(ref_rect.Min.x + ImMin(g.Style.FramePadding.x * 4, ref_rect.GetWidth()), ref_rect.Max.y - ImMin(g.Style.FramePadding.y, ref_rect.GetHeight())); + ImGuiViewport* viewport = GetMainViewport(); + return ImTrunc(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImTrunc() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. + } +} + +float ImGui::GetNavTweakPressedAmount(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(ImGuiInputFlags_RepeatRateNavTweak, &repeat_delay, &repeat_rate); + + ImGuiKey key_less, key_more; + if (g.NavInputSource == ImGuiInputSource_Gamepad) + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadLeft : ImGuiKey_GamepadDpadUp; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadRight : ImGuiKey_GamepadDpadDown; + } + else + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_LeftArrow : ImGuiKey_UpArrow; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_RightArrow : ImGuiKey_DownArrow; + } + float amount = (float)GetKeyPressedAmount(key_more, repeat_delay, repeat_rate) - (float)GetKeyPressedAmount(key_less, repeat_delay, repeat_rate); + if (amount != 0.0f && IsKeyDown(key_less) && IsKeyDown(key_more)) // Cancel when opposite directions are held, regardless of repeat phase + amount = 0.0f; + return amount; +} + +static void ImGui::NavUpdate() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + io.WantSetMousePos = false; + //if (g.NavScoringDebugCount > 0) IMGUI_DEBUG_LOG_NAV("[nav] NavScoringDebugCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.NavScoringDebugCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); + + // Set input source based on which keys are last pressed (as some features differs when used with Gamepad vs Keyboard) + // FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource? + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const ImGuiKey nav_gamepad_keys_to_change_source[] = { ImGuiKey_GamepadFaceRight, ImGuiKey_GamepadFaceLeft, ImGuiKey_GamepadFaceUp, ImGuiKey_GamepadFaceDown, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown }; + if (nav_gamepad_active) + for (ImGuiKey key : nav_gamepad_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Gamepad; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const ImGuiKey nav_keyboard_keys_to_change_source[] = { ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow }; + if (nav_keyboard_active) + for (ImGuiKey key : nav_keyboard_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Keyboard; + + // Process navigation init request (select first/default focus) + g.NavJustMovedToId = 0; + g.NavJustMovedToFocusScopeId = g.NavJustMovedFromFocusScopeId = 0; + if (g.NavInitResult.ID != 0) + NavInitRequestApplyResult(); + g.NavInitRequest = false; + g.NavInitRequestFromMove = false; + g.NavInitResult.ID = 0; + + // Process navigation move request + if (g.NavMoveSubmitted) + NavMoveRequestApplyResult(); + g.NavTabbingCounter = 0; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + + // Schedule mouse position update (will be done at the bottom of this function, after 1) processing all move requests and 2) updating scrolling) + bool set_mouse_pos = false; + if (g.NavMousePosDirty && g.NavIdIsAlive) + if (!g.NavDisableHighlight && g.NavDisableMouseHover && g.NavWindow) + set_mouse_pos = true; + g.NavMousePosDirty = false; + IM_ASSERT(g.NavLayer == ImGuiNavLayer_Main || g.NavLayer == ImGuiNavLayer_Menu); + + // Store our return window (for returning from Menu Layer to Main Layer) and clear it as soon as we step back in our own Layer 0 + if (g.NavWindow) + NavSaveLastChildNavWindowIntoParent(g.NavWindow); + if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == ImGuiNavLayer_Main) + g.NavWindow->NavLastChildNavWindow = NULL; + + // Update CTRL+TAB and Windowing features (hold Square to move/resize/etc.) + NavUpdateWindowing(); + + // Set output flags for user application + io.NavActive = (nav_keyboard_active || nav_gamepad_active) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); + io.NavVisible = (io.NavActive && g.NavId != 0 && !g.NavDisableHighlight) || (g.NavWindowingTarget != NULL); + + // Process NavCancel input (to close a popup, get back to parent, clear focus) + NavUpdateCancelRequest(); + + // Process manual activation request + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = 0; + g.NavActivateFlags = ImGuiActivateFlags_None; + if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + { + const bool activate_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Space, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadActivate, ImGuiKeyOwner_NoOwner)); + const bool activate_pressed = activate_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Space, 0, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, 0, ImGuiKeyOwner_NoOwner))); + const bool input_down = (nav_keyboard_active && (IsKeyDown(ImGuiKey_Enter, ImGuiKeyOwner_NoOwner) || IsKeyDown(ImGuiKey_KeypadEnter, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadInput, ImGuiKeyOwner_NoOwner)); + const bool input_pressed = input_down && ((nav_keyboard_active && (IsKeyPressed(ImGuiKey_Enter, 0, ImGuiKeyOwner_NoOwner) || IsKeyPressed(ImGuiKey_KeypadEnter, 0, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadInput, 0, ImGuiKeyOwner_NoOwner))); + if (g.ActiveId == 0 && activate_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferTweak; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && input_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_down || input_down)) + g.NavActivateDownId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_pressed || input_pressed)) + { + g.NavActivatePressedId = g.NavId; + NavHighlightActivated(g.NavId); + } + } + if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + g.NavDisableHighlight = true; + if (g.NavActivateId != 0) + IM_ASSERT(g.NavActivateDownId == g.NavActivateId); + + // Highlight + if (g.NavHighlightActivatedTimer > 0.0f) + g.NavHighlightActivatedTimer = ImMax(0.0f, g.NavHighlightActivatedTimer - io.DeltaTime); + if (g.NavHighlightActivatedTimer == 0.0f) + g.NavHighlightActivatedId = 0; + + // Process programmatic activation request + // FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others) + if (g.NavNextActivateId != 0) + { + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavNextActivateId; + g.NavActivateFlags = g.NavNextActivateFlags; + } + g.NavNextActivateId = 0; + + // Process move requests + NavUpdateCreateMoveRequest(); + if (g.NavMoveDir == ImGuiDir_None) + NavUpdateCreateTabbingRequest(); + NavUpdateAnyRequestFlag(); + g.NavIdIsAlive = false; + + // Scrolling + if (g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) + { + // *Fallback* manual-scroll with Nav directional keys when window has no navigable item + ImGuiWindow* window = g.NavWindow; + const float scroll_speed = IM_ROUND(window->CalcFontSize() * 100 * io.DeltaTime); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. + const ImGuiDir move_dir = g.NavMoveDir; + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY && move_dir != ImGuiDir_None) + { + if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) + SetScrollX(window, ImTrunc(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) + SetScrollY(window, ImTrunc(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + } + + // *Normal* Manual scroll with LStick + // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. + if (nav_gamepad_active) + { + const ImVec2 scroll_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + const float tweak_factor = IsKeyDown(ImGuiKey_NavGamepadTweakSlow) ? 1.0f / 10.0f : IsKeyDown(ImGuiKey_NavGamepadTweakFast) ? 10.0f : 1.0f; + if (scroll_dir.x != 0.0f && window->ScrollbarX) + SetScrollX(window, ImTrunc(window->Scroll.x + scroll_dir.x * scroll_speed * tweak_factor)); + if (scroll_dir.y != 0.0f) + SetScrollY(window, ImTrunc(window->Scroll.y + scroll_dir.y * scroll_speed * tweak_factor)); + } + } + + // Always prioritize mouse highlight if navigation is disabled + if (!nav_keyboard_active && !nav_gamepad_active) + { + g.NavDisableHighlight = true; + g.NavDisableMouseHover = set_mouse_pos = false; + } + + // Update mouse position if requested + // (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied) + if (set_mouse_pos && (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) && (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) + TeleportMousePos(NavCalcPreferredRefPos()); + + // [DEBUG] + g.NavScoringDebugCount = 0; +#if IMGUI_DEBUG_NAV_RECTS + if (ImGuiWindow* debug_window = g.NavWindow) + { + ImDrawList* draw_list = GetForegroundDrawList(debug_window); + int layer = g.NavLayer; /* for (int layer = 0; layer < 2; layer++)*/ { ImRect r = WindowRectRelToAbs(debug_window, debug_window->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 200, 0, 255)); } + //if (1) { ImU32 col = (!debug_window->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } + } +#endif +} + +void ImGui::NavInitRequestApplyResult() +{ + // In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void) + ImGuiContext& g = *GImGui; + if (!g.NavWindow) + return; + + ImGuiNavItemData* result = &g.NavInitResult; + if (g.NavId != result->ID) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = 0; + g.NavJustMovedToIsTabbing = false; + g.NavJustMovedToHasSelectionData = (result->InFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + } + + // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) + // FIXME-NAV: On _NavFlattened windows, g.NavWindow will only be updated during subsequent frame. Not a problem currently. + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: ApplyResult: NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + g.NavIdIsAlive = true; // Mark as alive from previous frame as we got a result + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + if (g.NavInitRequestFromMove) + NavRestoreHighlightAfterMove(); +} + +// Bias scoring rect ahead of scoring + update preferred pos (if missing) using source position +static void NavBiasScoringRect(ImRect& r, ImVec2& preferred_pos_rel, ImGuiDir move_dir, ImGuiNavMoveFlags move_flags) +{ + // Bias initial rect + ImGuiContext& g = *GImGui; + const ImVec2 rel_to_abs_offset = g.NavWindow->DC.CursorStartPos; + + // Initialize bias on departure if we don't have any. So mouse-click + arrow will record bias. + // - We default to L/U bias, so moving down from a large source item into several columns will land on left-most column. + // - But each successful move sets new bias on one axis, only cleared when using mouse. + if ((move_flags & ImGuiNavMoveFlags_Forwarded) == 0) + { + if (preferred_pos_rel.x == FLT_MAX) + preferred_pos_rel.x = ImMin(r.Min.x + 1.0f, r.Max.x) - rel_to_abs_offset.x; + if (preferred_pos_rel.y == FLT_MAX) + preferred_pos_rel.y = r.GetCenter().y - rel_to_abs_offset.y; + } + + // Apply general bias on the other axis + if ((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) && preferred_pos_rel.x != FLT_MAX) + r.Min.x = r.Max.x = preferred_pos_rel.x + rel_to_abs_offset.x; + else if ((move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) && preferred_pos_rel.y != FLT_MAX) + r.Min.y = r.Max.y = preferred_pos_rel.y + rel_to_abs_offset.y; +} + +void ImGui::NavUpdateCreateMoveRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiWindow* window = g.NavWindow; + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + + if (g.NavMoveForwardToNextFrame && window != NULL) + { + // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) + // (preserve most state, which were already set by the NavMoveRequestForward() function) + IM_ASSERT(g.NavMoveDir != ImGuiDir_None && g.NavMoveClipDir != ImGuiDir_None); + IM_ASSERT(g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded); + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestForward %d\n", g.NavMoveDir); + } + else + { + // Initiate directional inputs request + g.NavMoveDir = ImGuiDir_None; + g.NavMoveFlags = ImGuiNavMoveFlags_None; + g.NavMoveScrollFlags = ImGuiScrollFlags_None; + if (window && !g.NavWindowingTarget && !(window->Flags & ImGuiWindowFlags_NoNavInputs)) + { + const ImGuiInputFlags repeat_mode = ImGuiInputFlags_Repeat | (ImGuiInputFlags)ImGuiInputFlags_RepeatRateNavMove; + if (!IsActiveIdUsingNavDir(ImGuiDir_Left) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadLeft, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_LeftArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Left; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Right) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadRight, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_RightArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Right; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Up) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadUp, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_UpArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Up; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadDown, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_DownArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Down; } + } + g.NavMoveClipDir = g.NavMoveDir; + g.NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + } + + // Update PageUp/PageDown/Home/End scroll + // FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags_NavEnableKeyboard flag? + float scoring_rect_offset_y = 0.0f; + if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active) + scoring_rect_offset_y = NavUpdatePageUpPageDown(); + if (scoring_rect_offset_y != 0.0f) + { + g.NavScoringNoClipRect = window->InnerRect; + g.NavScoringNoClipRect.TranslateY(scoring_rect_offset_y); + } + + // [DEBUG] Always send a request when holding CTRL. Hold CTRL + Arrow change the direction. +#if IMGUI_DEBUG_NAV_SCORING + //if (io.KeyCtrl && IsKeyPressed(ImGuiKey_C)) + // g.NavMoveDirForDebug = (ImGuiDir)((g.NavMoveDirForDebug + 1) & 3); + if (io.KeyCtrl) + { + if (g.NavMoveDir == ImGuiDir_None) + g.NavMoveDir = g.NavMoveDirForDebug; + g.NavMoveClipDir = g.NavMoveDir; + g.NavMoveFlags |= ImGuiNavMoveFlags_DebugNoResult; + } +#endif + + // Submit + g.NavMoveForwardToNextFrame = false; + if (g.NavMoveDir != ImGuiDir_None) + NavMoveRequestSubmit(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); + + // Moving with no reference triggers an init request (will be used as a fallback if the direction fails to find a match) + if (g.NavMoveSubmitted && g.NavId == 0) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from move, window \"%s\", layer=%d\n", window ? window->Name : "", g.NavLayer); + g.NavInitRequest = g.NavInitRequestFromMove = true; + g.NavInitResult.ID = 0; + g.NavDisableHighlight = false; + } + + // When using gamepad, we project the reference nav bounding box into window visible area. + // This is to allow resuming navigation inside the visible area after doing a large amount of scrolling, + // since with gamepad all movements are relative (can't focus a visible object like we can with the mouse). + if (g.NavMoveSubmitted && g.NavInputSource == ImGuiInputSource_Gamepad && g.NavLayer == ImGuiNavLayer_Main && window != NULL)// && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded)) + { + bool clamp_x = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_WrapX)) == 0; + bool clamp_y = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapY)) == 0; + ImRect inner_rect_rel = WindowRectAbsToRel(window, ImRect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1))); + + // Take account of changing scroll to handle triggering a new move request on a scrolling frame. (#6171) + // Otherwise 'inner_rect_rel' would be off on the move result frame. + inner_rect_rel.Translate(CalcNextScrollFromScrollTargetAndClamp(window) - window->Scroll); + + if ((clamp_x || clamp_y) && !inner_rect_rel.Contains(window->NavRectRel[g.NavLayer])) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: clamp NavRectRel for gamepad move\n"); + float pad_x = ImMin(inner_rect_rel.GetWidth(), window->CalcFontSize() * 0.5f); + float pad_y = ImMin(inner_rect_rel.GetHeight(), window->CalcFontSize() * 0.5f); // Terrible approximation for the intent of starting navigation from first fully visible item + inner_rect_rel.Min.x = clamp_x ? (inner_rect_rel.Min.x + pad_x) : -FLT_MAX; + inner_rect_rel.Max.x = clamp_x ? (inner_rect_rel.Max.x - pad_x) : +FLT_MAX; + inner_rect_rel.Min.y = clamp_y ? (inner_rect_rel.Min.y + pad_y) : -FLT_MAX; + inner_rect_rel.Max.y = clamp_y ? (inner_rect_rel.Max.y - pad_y) : +FLT_MAX; + window->NavRectRel[g.NavLayer].ClipWithFull(inner_rect_rel); + g.NavId = 0; + } + } + + // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) + ImRect scoring_rect; + if (window != NULL) + { + ImRect nav_rect_rel = !window->NavRectRel[g.NavLayer].IsInverted() ? window->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0); + scoring_rect = WindowRectRelToAbs(window, nav_rect_rel); + scoring_rect.TranslateY(scoring_rect_offset_y); + if (g.NavMoveSubmitted) + NavBiasScoringRect(scoring_rect, window->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir, g.NavMoveFlags); + IM_ASSERT(!scoring_rect.IsInverted()); // Ensure we have a non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem(). + //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] + //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG] + } + g.NavScoringRect = scoring_rect; + g.NavScoringNoClipRect.Add(scoring_rect); +} + +void ImGui::NavUpdateCreateTabbingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + IM_ASSERT(g.NavMoveDir == ImGuiDir_None); + if (window == NULL || g.NavWindowingTarget != NULL || (window->Flags & ImGuiWindowFlags_NoNavInputs)) + return; + + const bool tab_pressed = IsKeyPressed(ImGuiKey_Tab, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner) && !g.IO.KeyCtrl && !g.IO.KeyAlt; + if (!tab_pressed) + return; + + // Initiate tabbing request + // (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!) + // See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping. + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (nav_keyboard_active) + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.NavDisableHighlight == true && g.ActiveId == 0) ? 0 : +1; + else + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1; + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down; + NavMoveRequestSubmit(ImGuiDir_None, clip_dir, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + g.NavTabbingCounter = -1; +} + +// Apply result from previous frame navigation directional move request. Always called from NavUpdate() +void ImGui::NavMoveRequestApplyResult() +{ + ImGuiContext& g = *GImGui; +#if IMGUI_DEBUG_NAV_SCORING + if (g.NavMoveFlags & ImGuiNavMoveFlags_DebugNoResult) // [DEBUG] Scoring all items in NavWindow at all times + return; +#endif + + // Select which result to use + ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL; + + // Tabbing forward wrap + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && result == NULL) + if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID) + result = &g.NavTabbingResultFirst; + + // In a situation when there are no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result) + const ImGuiAxis axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (result == NULL) + { + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavMoveFlags |= ImGuiNavMoveFlags_NoSetNavHighlight; + if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavHighlight) == 0) + NavRestoreHighlightAfterMove(); + NavClearPreferredPosForAxis(axis); // On a failed move, clear preferred pos for this axis. + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n"); + return; + } + + // PageUp/PageDown behavior first jumps to the bottom/top mostly visible item, _otherwise_ use the result from the previous/next page. + if (g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) + if (g.NavMoveResultLocalVisible.ID != 0 && g.NavMoveResultLocalVisible.ID != g.NavId) + result = &g.NavMoveResultLocalVisible; + + // Maybe entering a flattened child from the outside? In this case solve the tie using the regular scoring rules. + if (result != &g.NavMoveResultOther && g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) + if ((g.NavMoveResultOther.DistBox < result->DistBox) || (g.NavMoveResultOther.DistBox == result->DistBox && g.NavMoveResultOther.DistCenter < result->DistCenter)) + result = &g.NavMoveResultOther; + IM_ASSERT(g.NavWindow && result->Window); + + // Scroll to keep newly navigated item fully into view. + if (g.NavLayer == ImGuiNavLayer_Main) + { + ImRect rect_abs = WindowRectRelToAbs(result->Window, result->RectRel); + ScrollToRectEx(result->Window, rect_abs, g.NavMoveScrollFlags); + + if (g.NavMoveFlags & ImGuiNavMoveFlags_ScrollToEdgeY) + { + // FIXME: Should remove this? Or make more precise: use ScrollToRectEx() with edge? + float scroll_target = (g.NavMoveDir == ImGuiDir_Up) ? result->Window->ScrollMax.y : 0.0f; + SetScrollY(result->Window, scroll_target); + } + } + + if (g.NavWindow != result->Window) + { + IMGUI_DEBUG_LOG_FOCUS("[focus] NavMoveRequest: SetNavWindow(\"%s\")\n", result->Window->Name); + g.NavWindow = result->Window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + + // Clear active id unless requested not to + // FIXME: ImGuiNavMoveFlags_NoClearActiveId is currently unused as we don't have a clear strategy to preserve active id after interaction, + // so this is mostly provided as a gateway for further experiments (see #1418, #2890) + if (g.ActiveId != result->ID && (g.NavMoveFlags & ImGuiNavMoveFlags_NoClearActiveId) == 0) + ClearActiveID(); + + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + // PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior. + if ((g.NavId != result->ID || (g.NavMoveFlags & ImGuiNavMoveFlags_IsPageMove)) && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSelect) == 0) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = g.NavMoveKeyMods; + g.NavJustMovedToIsTabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + g.NavJustMovedToHasSelectionData = (result->InFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + //IMGUI_DEBUG_LOG_NAV("[nav] NavJustMovedFromFocusScopeId = 0x%08X, NavJustMovedToFocusScopeId = 0x%08X\n", g.NavJustMovedFromFocusScopeId, g.NavJustMovedToFocusScopeId); + } + + // Apply new NavID/Focus + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + ImVec2 preferred_scoring_pos_rel = g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer]; + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + + // Restore last preferred position for current axis + // (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) == 0) + { + preferred_scoring_pos_rel[axis] = result->RectRel.GetCenter()[axis]; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer] = preferred_scoring_pos_rel; + } + + // Tabbing: Activates Inputable, otherwise only Focus + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && (result->InFlags & ImGuiItemFlags_Inputable) == 0) + g.NavMoveFlags &= ~ImGuiNavMoveFlags_Activate; + + // Activate + if (g.NavMoveFlags & ImGuiNavMoveFlags_Activate) + { + g.NavNextActivateId = result->ID; + g.NavNextActivateFlags = ImGuiActivateFlags_None; + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavNextActivateFlags |= ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState | ImGuiActivateFlags_FromTabbing; + } + + // Enable nav highlight + if ((g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavHighlight) == 0) + NavRestoreHighlightAfterMove(); +} + +// Process NavCancel input (to close a popup, get back to parent, clear focus) +// FIXME: In order to support e.g. Escape to clear a selection we'll need: +// - either to store the equivalent of ActiveIdUsingKeyInputMask for a FocusScope and test for it. +// - either to move most/all of those tests to the epilogue/end functions of the scope they are dealing with (e.g. exit child window in EndChild()) or in EndFrame(), to allow an earlier intercept +static void ImGui::NavUpdateCancelRequest() +{ + ImGuiContext& g = *GImGui; + const bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (g.IO.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (!(nav_keyboard_active && IsKeyPressed(ImGuiKey_Escape, 0, ImGuiKeyOwner_NoOwner)) && !(nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadCancel, 0, ImGuiKeyOwner_NoOwner))) + return; + + IMGUI_DEBUG_LOG_NAV("[nav] NavUpdateCancelRequest()\n"); + if (g.ActiveId != 0) + { + ClearActiveID(); + } + else if (g.NavLayer != ImGuiNavLayer_Main) + { + // Leave the "menu" layer + NavRestoreLayer(ImGuiNavLayer_Main); + NavRestoreHighlightAfterMove(); + } + else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->RootWindowForNav->ParentWindow) + { + // Exit child window + ImGuiWindow* child_window = g.NavWindow->RootWindowForNav; + ImGuiWindow* parent_window = child_window->ParentWindow; + IM_ASSERT(child_window->ChildId != 0); + FocusWindow(parent_window); + SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_window->Rect())); + NavRestoreHighlightAfterMove(); + } + else if (g.OpenPopupStack.Size > 0 && g.OpenPopupStack.back().Window != NULL && !(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) + { + // Close open popup/menu + ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); + } + else + { + // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were + if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup) || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) + g.NavWindow->NavLastIds[0] = 0; + g.NavId = 0; + } +} + +// Handle PageUp/PageDown/Home/End keys +// Called from NavUpdateCreateMoveRequest() which will use our output to create a move request +// FIXME-NAV: This doesn't work properly with NavFlattened siblings as we use NavWindow rectangle for reference +// FIXME-NAV: how to get Home/End to aim at the beginning/end of a 2D grid? +static float ImGui::NavUpdatePageUpPageDown() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + if ((window->Flags & ImGuiWindowFlags_NoNavInputs) || g.NavWindowingTarget != NULL) + return 0.0f; + + const bool page_up_held = IsKeyDown(ImGuiKey_PageUp, ImGuiKeyOwner_NoOwner); + const bool page_down_held = IsKeyDown(ImGuiKey_PageDown, ImGuiKeyOwner_NoOwner); + const bool home_pressed = IsKeyPressed(ImGuiKey_Home, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + const bool end_pressed = IsKeyPressed(ImGuiKey_End, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + if (page_up_held == page_down_held && home_pressed == end_pressed) // Proceed if either (not both) are pressed, otherwise early out + return 0.0f; + + if (g.NavLayer != ImGuiNavLayer_Main) + NavRestoreLayer(ImGuiNavLayer_Main); + + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY) + { + // Fallback manual-scroll when window has no navigable item + if (IsKeyPressed(ImGuiKey_PageUp, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y - window->InnerRect.GetHeight()); + else if (IsKeyPressed(ImGuiKey_PageDown, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y + window->InnerRect.GetHeight()); + else if (home_pressed) + SetScrollY(window, 0.0f); + else if (end_pressed) + SetScrollY(window, window->ScrollMax.y); + } + else + { + ImRect& nav_rect_rel = window->NavRectRel[g.NavLayer]; + const float page_offset_y = ImMax(0.0f, window->InnerRect.GetHeight() - window->CalcFontSize() * 1.0f + nav_rect_rel.GetHeight()); + float nav_scoring_rect_offset_y = 0.0f; + if (IsKeyPressed(ImGuiKey_PageUp, true)) + { + nav_scoring_rect_offset_y = -page_offset_y; + g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; + } + else if (IsKeyPressed(ImGuiKey_PageDown, true)) + { + nav_scoring_rect_offset_y = +page_offset_y; + g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; + } + else if (home_pressed) + { + // FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y + // Scrolling will be handled via the ImGuiNavMoveFlags_ScrollToEdgeY flag, we don't scroll immediately to avoid scrolling happening before nav result. + // Preserve current horizontal position if we have any. + nav_rect_rel.Min.y = nav_rect_rel.Max.y = 0.0f; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + else if (end_pressed) + { + nav_rect_rel.Min.y = nav_rect_rel.Max.y = window->ContentSize.y; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + return nav_scoring_rect_offset_y; + } + return 0.0f; +} + +static void ImGui::NavEndFrame() +{ + ImGuiContext& g = *GImGui; + + // Show CTRL+TAB list window + if (g.NavWindowingTarget != NULL) + NavUpdateWindowingOverlay(); + + // Perform wrap-around in menus + // FIXME-NAV: Wrap may need to apply a weight bias on the other axis. e.g. 4x4 grid with 2 last items missing on last item won't handle LoopY/WrapY correctly. + // FIXME-NAV: Wrap (not Loop) support could be handled by the scoring function and then WrapX would function without an extra frame. + if (g.NavWindow && NavMoveRequestButNoResultYet() && (g.NavMoveFlags & ImGuiNavMoveFlags_WrapMask_) && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + NavUpdateCreateWrappingRequest(); +} + +static void ImGui::NavUpdateCreateWrappingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + + bool do_forward = false; + ImRect bb_rel = window->NavRectRel[g.NavLayer]; + ImGuiDir clip_dir = g.NavMoveDir; + + const ImGuiNavMoveFlags move_flags = g.NavMoveFlags; + //const ImGuiAxis move_axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (g.NavMoveDir == ImGuiDir_Left && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = window->ContentSize.x + window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(-bb_rel.GetHeight()); // Previous row + clip_dir = ImGuiDir_Up; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Right && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = -window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(+bb_rel.GetHeight()); // Next row + clip_dir = ImGuiDir_Down; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Up && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = window->ContentSize.y + window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(-bb_rel.GetWidth()); // Previous column + clip_dir = ImGuiDir_Left; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Down && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = -window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(+bb_rel.GetWidth()); // Next column + clip_dir = ImGuiDir_Right; + } + do_forward = true; + } + if (!do_forward) + return; + window->NavRectRel[g.NavLayer] = bb_rel; + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags); +} + +static int ImGui::FindWindowFocusIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(g); + int order = window->FocusOrder; + IM_ASSERT(window->RootWindow == window); // No child window (not testing _ChildWindow because of docking) + IM_ASSERT(g.WindowsFocusOrder[order] == window); + return order; +} + +static ImGuiWindow* FindWindowNavFocusable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +{ + ImGuiContext& g = *GImGui; + for (int i = i_start; i >= 0 && i < g.WindowsFocusOrder.Size && i != i_stop; i += dir) + if (ImGui::IsWindowNavFocusable(g.WindowsFocusOrder[i])) + return g.WindowsFocusOrder[i]; + return NULL; +} + +static void NavUpdateWindowingHighlightWindow(int focus_change_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget); + if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) + return; + + const int i_current = ImGui::FindWindowFocusIndex(g.NavWindowingTarget); + ImGuiWindow* window_target = FindWindowNavFocusable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); + if (!window_target) + window_target = FindWindowNavFocusable((focus_change_dir < 0) ? (g.WindowsFocusOrder.Size - 1) : 0, i_current, focus_change_dir); + if (window_target) // Don't reset windowing target if there's a single window in the list + { + g.NavWindowingTarget = g.NavWindowingTargetAnim = window_target; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + } + g.NavWindowingToggleLayer = false; +} + +// Windowing management mode +// Keyboard: CTRL+Tab (change focus/move/resize), Alt (toggle menu layer) +// Gamepad: Hold Menu/Square (change focus/move/resize), Tap Menu/Square (toggle menu layer) +static void ImGui::NavUpdateWindowing() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + ImGuiWindow* apply_focus_window = NULL; + bool apply_toggle_layer = false; + + ImGuiWindow* modal_window = GetTopMostPopupModal(); + bool allow_windowing = (modal_window == NULL); // FIXME: This prevent CTRL+TAB from being usable with windows that are inside the Begin-stack of that modal. + if (!allow_windowing) + g.NavWindowingTarget = NULL; + + // Fade out + if (g.NavWindowingTargetAnim && g.NavWindowingTarget == NULL) + { + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - io.DeltaTime * 10.0f, 0.0f); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + g.NavWindowingTargetAnim = NULL; + } + + // Start CTRL+Tab or Square+L/R window selection + // (g.ConfigNavWindowingKeyNext/g.ConfigNavWindowingKeyPrev defaults are ImGuiMod_Ctrl|ImGuiKey_Tab and ImGuiMod_Ctrl|ImGuiMod_Shift|ImGuiKey_Tab) + const ImGuiID owner_id = ImHashStr("###NavUpdateWindowing"); + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const bool keyboard_next_window = allow_windowing && g.ConfigNavWindowingKeyNext && Shortcut(g.ConfigNavWindowingKeyNext, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool keyboard_prev_window = allow_windowing && g.ConfigNavWindowingKeyPrev && Shortcut(g.ConfigNavWindowingKeyPrev, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool start_windowing_with_gamepad = allow_windowing && nav_gamepad_active && !g.NavWindowingTarget && IsKeyPressed(ImGuiKey_NavGamepadMenu, ImGuiInputFlags_None); + const bool start_windowing_with_keyboard = allow_windowing && !g.NavWindowingTarget && (keyboard_next_window || keyboard_prev_window); // Note: enabled even without NavEnableKeyboard! + if (start_windowing_with_gamepad || start_windowing_with_keyboard) + if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1)) + { + g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; + g.NavWindowingTimer = g.NavWindowingHighlightAlpha = 0.0f; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + g.NavWindowingToggleLayer = start_windowing_with_gamepad ? true : false; // Gamepad starts toggling layer + g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad; + + // Manually register ownership of our mods. Using a global route in the Shortcut() calls instead would probably be correct but may have more side-effects. + if (keyboard_next_window || keyboard_prev_window) + SetKeyOwnersForKeyChord((g.ConfigNavWindowingKeyNext | g.ConfigNavWindowingKeyPrev) & ImGuiMod_Mask_, owner_id); + } + + // Gamepad update + g.NavWindowingTimer += io.DeltaTime; + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Gamepad) + { + // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); + + // Select window to focus + const int focus_change_dir = (int)IsKeyPressed(ImGuiKey_GamepadL1) - (int)IsKeyPressed(ImGuiKey_GamepadR1); + if (focus_change_dir != 0) + { + NavUpdateWindowingHighlightWindow(focus_change_dir); + g.NavWindowingHighlightAlpha = 1.0f; + } + + // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most) + if (!IsKeyDown(ImGuiKey_NavGamepadMenu)) + { + g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. + if (g.NavWindowingToggleLayer && g.NavWindow) + apply_toggle_layer = true; + else if (!g.NavWindowingToggleLayer) + apply_focus_window = g.NavWindowingTarget; + g.NavWindowingTarget = NULL; + } + } + + // Keyboard: Focus + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise + ImGuiKeyChord shared_mods = ((g.ConfigNavWindowingKeyNext ? g.ConfigNavWindowingKeyNext : ImGuiMod_Mask_) & (g.ConfigNavWindowingKeyPrev ? g.ConfigNavWindowingKeyPrev : ImGuiMod_Mask_)) & ImGuiMod_Mask_; + IM_ASSERT(shared_mods != 0); // Next/Prev shortcut currently needs a shared modifier to "hold", otherwise Prev actions would keep cycling between two windows. + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // 1.0f + if (keyboard_next_window || keyboard_prev_window) + NavUpdateWindowingHighlightWindow(keyboard_next_window ? -1 : +1); + else if ((io.KeyMods & shared_mods) != shared_mods) + apply_focus_window = g.NavWindowingTarget; + } + + // Keyboard: Press and Release ALT to toggle menu layer + const ImGuiKey windowing_toggle_keys[] = { ImGuiKey_LeftAlt, ImGuiKey_RightAlt }; + for (ImGuiKey windowing_toggle_key : windowing_toggle_keys) + if (nav_keyboard_active && IsKeyPressed(windowing_toggle_key, 0, ImGuiKeyOwner_NoOwner)) + { + g.NavWindowingToggleLayer = true; + g.NavWindowingToggleKey = windowing_toggle_key; + g.NavInputSource = ImGuiInputSource_Keyboard; + break; + } + if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370) + // We cancel toggling nav layer when other modifiers are pressed. (See #4439) + // - AltGR is Alt+Ctrl on some layout but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). + // We cancel toggling nav layer if an owner has claimed the key. + if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper) + g.NavWindowingToggleLayer = false; + if (TestKeyOwner(g.NavWindowingToggleKey, ImGuiKeyOwner_NoOwner) == false || TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_NoOwner) == false) + g.NavWindowingToggleLayer = false; + + // Apply layer toggle on Alt release + // Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss. + if (IsKeyReleased(g.NavWindowingToggleKey) && g.NavWindowingToggleLayer) + if (g.ActiveId == 0 || g.ActiveIdAllowOverlap) + if (IsMousePosValid(&io.MousePos) == IsMousePosValid(&io.MousePosPrev)) + apply_toggle_layer = true; + if (!IsKeyDown(g.NavWindowingToggleKey)) + g.NavWindowingToggleLayer = false; + } + + // Move window + if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) + { + ImVec2 nav_move_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && !io.KeyShift) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + if (nav_move_dir.x != 0.0f || nav_move_dir.y != 0.0f) + { + const float NAV_MOVE_SPEED = 800.0f; + const float move_step = NAV_MOVE_SPEED * io.DeltaTime * ImMin(io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaPos += nav_move_dir * move_step; + g.NavDisableMouseHover = true; + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaPos); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindow; + SetWindowPos(moving_window, moving_window->Pos + accum_floored, ImGuiCond_Always); + g.NavWindowingAccumDeltaPos -= accum_floored; + } + } + } + + // Apply final focus + if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow)) + { + ClearActiveID(); + NavRestoreHighlightAfterMove(); + ClosePopupsOverWindow(apply_focus_window, false); + FocusWindow(apply_focus_window, ImGuiFocusRequestFlags_RestoreFocusedChild); + apply_focus_window = g.NavWindow; + if (apply_focus_window->NavLastIds[0] == 0) + NavInitWindow(apply_focus_window, false); + + // If the window has ONLY a menu layer (no main layer), select it directly + // Use NavLayersActiveMaskNext since windows didn't have a chance to be Begin()-ed on this frame, + // so CTRL+Tab where the keys are only held for 1 frame will be able to use correct layers mask since + // the target window as already been previewed once. + // FIXME-NAV: This should be done in NavInit.. or in FocusWindow... However in both of those cases, + // we won't have a guarantee that windows has been visible before and therefore NavLayersActiveMask* + // won't be valid. + if (apply_focus_window->DC.NavLayersActiveMaskNext == (1 << ImGuiNavLayer_Menu)) + g.NavLayer = ImGuiNavLayer_Menu; + } + if (apply_focus_window) + g.NavWindowingTarget = NULL; + + // Apply menu/layer toggle + if (apply_toggle_layer && g.NavWindow) + { + ClearActiveID(); + + // Move to parent menu if necessary + ImGuiWindow* new_nav_window = g.NavWindow; + while (new_nav_window->ParentWindow + && (new_nav_window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0 + && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 + && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + new_nav_window = new_nav_window->ParentWindow; + if (new_nav_window != g.NavWindow) + { + ImGuiWindow* old_nav_window = g.NavWindow; + FocusWindow(new_nav_window); + new_nav_window->NavLastChildNavWindow = old_nav_window; + } + + // Toggle layer + const ImGuiNavLayer new_nav_layer = (g.NavWindow->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) ? (ImGuiNavLayer)((int)g.NavLayer ^ 1) : ImGuiNavLayer_Main; + if (new_nav_layer != g.NavLayer) + { + // Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?) + if (new_nav_layer == ImGuiNavLayer_Menu) + g.NavWindow->NavLastIds[new_nav_layer] = 0; + NavRestoreLayer(new_nav_layer); + NavRestoreHighlightAfterMove(); + } + } +} + +// Window has already passed the IsWindowNavFocusable() +static const char* GetFallbackWindowNameForWindowingList(ImGuiWindow* window) +{ + if (window->Flags & ImGuiWindowFlags_Popup) + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingPopup); + if ((window->Flags & ImGuiWindowFlags_MenuBar) && strcmp(window->Name, "##MainMenuBar") == 0) + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingMainMenuBar); + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingUntitled); +} + +// Overlay displayed when using CTRL+TAB. Called by EndFrame(). +void ImGui::NavUpdateWindowingOverlay() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget != NULL); + + if (g.NavWindowingTimer < NAV_WINDOWING_LIST_APPEAR_DELAY) + return; + + if (g.NavWindowingListWindow == NULL) + g.NavWindowingListWindow = FindWindowByName("###NavWindowingList"); + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowSizeConstraints(ImVec2(viewport->Size.x * 0.20f, viewport->Size.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.WindowPadding * 2.0f); + Begin("###NavWindowingList", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + if (g.ContextName[0] != 0) + SeparatorText(g.ContextName); + for (int n = g.WindowsFocusOrder.Size - 1; n >= 0; n--) + { + ImGuiWindow* window = g.WindowsFocusOrder[n]; + IM_ASSERT(window != NULL); // Fix static analyzers + if (!IsWindowNavFocusable(window)) + continue; + const char* label = window->Name; + if (label == FindRenderedTextEnd(label)) + label = GetFallbackWindowNameForWindowingList(window); + Selectable(label, g.NavWindowingTarget == window); + } + End(); + PopStyleVar(); +} + + +//----------------------------------------------------------------------------- +// [SECTION] DRAG AND DROP +//----------------------------------------------------------------------------- + +bool ImGui::IsDragDropActive() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive; +} + +void ImGui::ClearDragDrop() +{ + ImGuiContext& g = *GImGui; + if (g.DragDropActive) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] ClearDragDrop()\n"); + g.DragDropActive = false; + g.DragDropPayload.Clear(); + g.DragDropAcceptFlags = ImGuiDragDropFlags_None; + g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropAcceptFrameCount = -1; + + g.DragDropPayloadBufHeap.clear(); + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); +} + +bool ImGui::BeginTooltipHidden() +{ + ImGuiContext& g = *GImGui; + bool ret = Begin("##Tooltip_Hidden", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.CurrentWindow); + return ret; +} + +// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() +// If the item has an identifier: +// - This assume/require the item to be activated (typically via ButtonBehavior). +// - Therefore if you want to use this with a mouse button other than left mouse button, it is up to the item itself to activate with another button. +// - We then pull and use the mouse button that was used to activate the item and use it to carry on the drag. +// If the item has no identifier: +// - Currently always assume left mouse button. +bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // FIXME-DRAGDROP: While in the common-most "drag from non-zero active id" case we can tell the mouse button, + // in both SourceExtern and id==0 cases we may requires something else (explicit flags or some heuristic). + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left; + + bool source_drag_active = false; + ImGuiID source_id = 0; + ImGuiID source_parent_id = 0; + if ((flags & ImGuiDragDropFlags_SourceExtern) == 0) + { + source_id = g.LastItemData.ID; + if (source_id != 0) + { + // Common path: items with ID + if (g.ActiveId != source_id) + return false; + if (g.ActiveIdMouseButton != -1) + mouse_button = g.ActiveIdMouseButton; + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + g.ActiveIdAllowOverlap = false; + } + else + { + // Uncommon path: items without ID + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window)) + return false; + + // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: + // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag. + if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) + { + IM_ASSERT(0); + return false; + } + + // Magic fallback to handle items with no assigned ID, e.g. Text(), Image() + // We build a throwaway ID based on current ID stack + relative AABB of items in window. + // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING/RESIZINGG OF THE WIDGET, so if your widget moves your dragging operation will be canceled. + // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. + // Rely on keeping other window->LastItemXXX fields intact. + source_id = g.LastItemData.ID = window->GetIDFromRectangle(g.LastItemData.Rect); + KeepAliveID(source_id); + bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id, g.LastItemData.InFlags); + if (is_hovered && g.IO.MouseClicked[mouse_button]) + { + SetActiveID(source_id, window); + FocusWindow(window); + } + if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. + g.ActiveIdAllowOverlap = is_hovered; + } + if (g.ActiveId != source_id) + return false; + source_parent_id = window->IDStack.back(); + source_drag_active = IsMouseDragging(mouse_button); + + // Disable navigation and key inputs while dragging + cancel existing request if any + SetActiveIdUsingAllKeyboardKeys(); + } + else + { + // When ImGuiDragDropFlags_SourceExtern is set: + window = NULL; + source_id = ImHashStr("#SourceExtern"); + source_drag_active = true; + mouse_button = g.IO.MouseDown[0] ? 0 : -1; + KeepAliveID(source_id); + SetActiveID(source_id, NULL); + } + + IM_ASSERT(g.DragDropWithinTarget == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + if (!source_drag_active) + return false; + + // Activate drag and drop + if (!g.DragDropActive) + { + IM_ASSERT(source_id != 0); + ClearDragDrop(); + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] BeginDragDropSource() DragDropActive = true, source_id = 0x%08X%s\n", + source_id, (flags & ImGuiDragDropFlags_SourceExtern) ? " (EXTERN)" : ""); + ImGuiPayload& payload = g.DragDropPayload; + payload.SourceId = source_id; + payload.SourceParentId = source_parent_id; + g.DragDropActive = true; + g.DragDropSourceFlags = flags; + g.DragDropMouseButton = mouse_button; + if (payload.SourceId == g.ActiveId) + g.ActiveIdNoClearOnFocusLoss = true; + } + g.DragDropSourceFrameCount = g.FrameCount; + g.DragDropWithinSource = true; + + if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) + // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. + bool ret; + if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) + ret = BeginTooltipHidden(); + else + ret = BeginTooltip(); + IM_ASSERT(ret); // FIXME-NEWBEGIN: If this ever becomes false, we need to Begin("##Hidden", NULL, ImGuiWindowFlags_NoSavedSettings) + SetWindowHiddendAndSkipItemsForCurrentFrame(). + IM_UNUSED(ret); + } + + if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) + g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; + + return true; +} + +void ImGui::EndDragDropSource() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinSource && "Not after a BeginDragDropSource()?"); + + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + EndTooltip(); + + // Discard the drag if have not called SetDragDropPayload() + if (g.DragDropPayload.DataFrameCount == -1) + ClearDragDrop(); + g.DragDropWithinSource = false; +} + +// Use 'cond' to choose to submit payload on drag start or every frame +bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + if (cond == 0) + cond = ImGuiCond_Always; + + IM_ASSERT(type != NULL); + IM_ASSERT(strlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 32 characters long"); + IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); + IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); + IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() + + if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) + { + // Copy payload + ImStrncpy(payload.DataType, type, IM_ARRAYSIZE(payload.DataType)); + g.DragDropPayloadBufHeap.resize(0); + if (data_size > sizeof(g.DragDropPayloadBufLocal)) + { + // Store in heap + g.DragDropPayloadBufHeap.resize((int)data_size); + payload.Data = g.DragDropPayloadBufHeap.Data; + memcpy(payload.Data, data, data_size); + } + else if (data_size > 0) + { + // Store locally + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); + payload.Data = g.DragDropPayloadBufLocal; + memcpy(payload.Data, data, data_size); + } + else + { + payload.Data = NULL; + } + payload.DataSize = (int)data_size; + } + payload.DataFrameCount = g.FrameCount; + + // Return whether the payload has been accepted + return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); +} + +bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow) + return false; + IM_ASSERT(id != 0); + if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) + return false; + if (window->SkipItems) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + g.DragDropTargetRect = bb; + g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case? + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +// We don't use BeginDragDropTargetCustom() and duplicate its code because: +// 1) we use LastItemData's ImGuiItemStatusFlags_HoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. +// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) +bool ImGui::BeginDragDropTarget() +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (!(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow || window->SkipItems) + return false; + + const ImRect& display_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? g.LastItemData.DisplayRect : g.LastItemData.Rect; + ImGuiID id = g.LastItemData.ID; + if (id == 0) + { + id = window->GetIDFromRectangle(display_rect); + KeepAliveID(id); + } + if (g.DragDropPayload.SourceId == id) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + g.DragDropTargetRect = display_rect; + g.DragDropTargetClipRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +bool ImGui::IsDragDropPayloadBeingAccepted() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive && g.DragDropAcceptIdPrev != 0; +} + +const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? + IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? + if (type != NULL && !payload.IsDataType(type)) + return NULL; + + // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. + // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! + const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); + ImRect r = g.DragDropTargetRect; + float r_surface = r.GetWidth() * r.GetHeight(); + if (r_surface > g.DragDropAcceptIdCurrRectSurface) + return NULL; + + g.DragDropAcceptFlags = flags; + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + //IMGUI_DEBUG_LOG("AcceptDragDropPayload(): %08X: accept\n", g.DragDropTargetId); + + // Render default drop visuals + payload.Preview = was_accepted_previously; + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that live for 1 frame) + if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) + RenderDragDropTargetRect(r, g.DragDropTargetClipRect); + + g.DragDropAcceptFrameCount = g.FrameCount; + if ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) && g.DragDropMouseButton == -1) + payload.Delivery = was_accepted_previously && (g.DragDropSourceFrameCount < g.FrameCount); + else + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting OS window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) + return NULL; + + if (payload.Delivery) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] AcceptDragDropPayload(): 0x%08X: payload delivery\n", g.DragDropTargetId); + return &payload; +} + +// FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target. +void ImGui::RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImRect bb_display = bb; + bb_display.ClipWith(item_clip_rect); // Clip THEN expand so we have a way to visualize that target is not entirely visible. + bb_display.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(bb_display); + if (push_clip_rect) + window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(bb_display.Min, bb_display.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + if (push_clip_rect) + window->DrawList->PopClipRect(); +} + +const ImGuiPayload* ImGui::GetDragDropPayload() +{ + ImGuiContext& g = *GImGui; + return (g.DragDropActive && g.DragDropPayload.DataFrameCount != -1) ? &g.DragDropPayload : NULL; +} + +void ImGui::EndDragDropTarget() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinTarget); + g.DragDropWithinTarget = false; + + // Clear drag and drop state payload right after delivery + if (g.DragDropPayload.Delivery) + ClearDragDrop(); +} + +//----------------------------------------------------------------------------- +// [SECTION] LOGGING/CAPTURING +//----------------------------------------------------------------------------- +// All text output from the interface can be captured into tty/file/clipboard. +// By default, tree nodes are automatically opened during logging. +//----------------------------------------------------------------------------- + +// Pass text data straight to log (without being displayed) +static inline void LogTextV(ImGuiContext& g, const char* fmt, va_list args) +{ + if (g.LogFile) + { + g.LogBuffer.Buf.resize(0); + g.LogBuffer.appendfv(fmt, args); + ImFileWrite(g.LogBuffer.c_str(), sizeof(char), (ImU64)g.LogBuffer.size(), g.LogFile); + } + else + { + g.LogBuffer.appendfv(fmt, args); + } +} + +void ImGui::LogText(const char* fmt, ...) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + va_list args; + va_start(args, fmt); + LogTextV(g, fmt, args); + va_end(args); +} + +void ImGui::LogTextV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogTextV(g, fmt, args); +} + +// Internal version that takes a position to decide on newline placement and pad items according to their depth. +// We split text into individual lines to add current tree level padding +// FIXME: This code is a little complicated perhaps, considering simplifying the whole system. +void ImGui::LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const char* prefix = g.LogNextPrefix; + const char* suffix = g.LogNextSuffix; + g.LogNextPrefix = g.LogNextSuffix = NULL; + + if (!text_end) + text_end = FindRenderedTextEnd(text, text_end); + + const bool log_new_line = ref_pos && (ref_pos->y > g.LogLinePosY + g.Style.FramePadding.y + 1); + if (ref_pos) + g.LogLinePosY = ref_pos->y; + if (log_new_line) + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + + if (prefix) + LogRenderedText(ref_pos, prefix, prefix + strlen(prefix)); // Calculate end ourself to ensure "##" are included here. + + // Re-adjust padding if we have popped out of our starting depth + if (g.LogDepthRef > window->DC.TreeDepth) + g.LogDepthRef = window->DC.TreeDepth; + const int tree_depth = (window->DC.TreeDepth - g.LogDepthRef); + + const char* text_remaining = text; + for (;;) + { + // Split the string. Each new line (after a '\n') is followed by indentation corresponding to the current depth of our log entry. + // We don't add a trailing \n yet to allow a subsequent item on the same line to be captured. + const char* line_start = text_remaining; + const char* line_end = ImStreolRange(line_start, text_end); + const bool is_last_line = (line_end == text_end); + if (line_start != line_end || !is_last_line) + { + const int line_length = (int)(line_end - line_start); + const int indentation = g.LogLineFirstItem ? tree_depth * 4 : 1; + LogText("%*s%.*s", indentation, "", line_length, line_start); + g.LogLineFirstItem = false; + if (*line_end == '\n') + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + } + if (is_last_line) + break; + text_remaining = line_end + 1; + } + + if (suffix) + LogRenderedText(ref_pos, suffix, suffix + strlen(suffix)); +} + +// Start logging/capturing text output +void ImGui::LogBegin(ImGuiLogType type, int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.LogEnabled == false); + IM_ASSERT(g.LogFile == NULL); + IM_ASSERT(g.LogBuffer.empty()); + g.LogEnabled = g.ItemUnclipByLog = true; + g.LogType = type; + g.LogNextPrefix = g.LogNextSuffix = NULL; + g.LogDepthRef = window->DC.TreeDepth; + g.LogDepthToExpand = ((auto_open_depth >= 0) ? auto_open_depth : g.LogDepthToExpandDefault); + g.LogLinePosY = FLT_MAX; + g.LogLineFirstItem = true; +} + +// Important: doesn't copy underlying data, use carefully (prefix/suffix must be in scope at the time of the next LogRenderedText) +void ImGui::LogSetNextTextDecoration(const char* prefix, const char* suffix) +{ + ImGuiContext& g = *GImGui; + g.LogNextPrefix = prefix; + g.LogNextSuffix = suffix; +} + +void ImGui::LogToTTY(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + IM_UNUSED(auto_open_depth); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + LogBegin(ImGuiLogType_TTY, auto_open_depth); + g.LogFile = stdout; +#endif +} + +// Start logging/capturing text output to given file +void ImGui::LogToFile(int auto_open_depth, const char* filename) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + + // FIXME: We could probably open the file in text mode "at", however note that clipboard/buffer logging will still + // be subject to outputting OS-incompatible carriage return if within strings the user doesn't use IM_NEWLINE. + // By opening the file in binary mode "ab" we have consistent output everywhere. + if (!filename) + filename = g.IO.LogFilename; + if (!filename || !filename[0]) + return; + ImFileHandle f = ImFileOpen(filename, "ab"); + if (!f) + { + IM_ASSERT(0); + return; + } + + LogBegin(ImGuiLogType_File, auto_open_depth); + g.LogFile = f; +} + +// Start logging/capturing text output to clipboard +void ImGui::LogToClipboard(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogType_Clipboard, auto_open_depth); +} + +void ImGui::LogToBuffer(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogType_Buffer, auto_open_depth); +} + +void ImGui::LogFinish() +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogText(IM_NEWLINE); + switch (g.LogType) + { + case ImGuiLogType_TTY: +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + fflush(g.LogFile); +#endif + break; + case ImGuiLogType_File: + ImFileClose(g.LogFile); + break; + case ImGuiLogType_Buffer: + break; + case ImGuiLogType_Clipboard: + if (!g.LogBuffer.empty()) + SetClipboardText(g.LogBuffer.begin()); + break; + case ImGuiLogType_None: + IM_ASSERT(0); + break; + } + + g.LogEnabled = g.ItemUnclipByLog = false; + g.LogType = ImGuiLogType_None; + g.LogFile = NULL; + g.LogBuffer.clear(); +} + +// Helper to display logging buttons +// FIXME-OBSOLETE: We should probably obsolete this and let the user have their own helper (this is one of the oldest function alive!) +void ImGui::LogButtons() +{ + ImGuiContext& g = *GImGui; + + PushID("LogButtons"); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + const bool log_to_tty = Button("Log To TTY"); SameLine(); +#else + const bool log_to_tty = false; +#endif + const bool log_to_file = Button("Log To File"); SameLine(); + const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); + PushItemFlag(ImGuiItemFlags_NoTabStop, true); + SetNextItemWidth(80.0f); + SliderInt("Default Depth", &g.LogDepthToExpandDefault, 0, 9, NULL); + PopItemFlag(); + PopID(); + + // Start logging at the end of the function so that the buttons don't appear in the log + if (log_to_tty) + LogToTTY(); + if (log_to_file) + LogToFile(); + if (log_to_clipboard) + LogToClipboard(); +} + + +//----------------------------------------------------------------------------- +// [SECTION] SETTINGS +//----------------------------------------------------------------------------- +// - UpdateSettings() [Internal] +// - MarkIniSettingsDirty() [Internal] +// - FindSettingsHandler() [Internal] +// - ClearIniSettings() [Internal] +// - LoadIniSettingsFromDisk() +// - LoadIniSettingsFromMemory() +// - SaveIniSettingsToDisk() +// - SaveIniSettingsToMemory() +//----------------------------------------------------------------------------- +// - CreateNewWindowSettings() [Internal] +// - FindWindowSettingsByID() [Internal] +// - FindWindowSettingsByWindow() [Internal] +// - ClearWindowSettings() [Internal] +// - WindowSettingsHandler_***() [Internal] +//----------------------------------------------------------------------------- + +// Called by NewFrame() +void ImGui::UpdateSettings() +{ + // Load settings on first frame (if not explicitly loaded manually before) + ImGuiContext& g = *GImGui; + if (!g.SettingsLoaded) + { + IM_ASSERT(g.SettingsWindows.empty()); + if (g.IO.IniFilename) + LoadIniSettingsFromDisk(g.IO.IniFilename); + g.SettingsLoaded = true; + } + + // Save settings (with a delay after the last modification, so we don't spam disk too much) + if (g.SettingsDirtyTimer > 0.0f) + { + g.SettingsDirtyTimer -= g.IO.DeltaTime; + if (g.SettingsDirtyTimer <= 0.0f) + { + if (g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + else + g.IO.WantSaveIniSettings = true; // Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves. + g.SettingsDirtyTimer = 0.0f; + } + } +} + +void ImGui::MarkIniSettingsDirty() +{ + ImGuiContext& g = *GImGui; + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +void ImGui::MarkIniSettingsDirty(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +void ImGui::AddSettingsHandler(const ImGuiSettingsHandler* handler) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(FindSettingsHandler(handler->TypeName) == NULL); + g.SettingsHandlers.push_back(*handler); +} + +void ImGui::RemoveSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + if (ImGuiSettingsHandler* handler = FindSettingsHandler(type_name)) + g.SettingsHandlers.erase(handler); +} + +ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + const ImGuiID type_hash = ImHashStr(type_name); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.TypeHash == type_hash) + return &handler; + return NULL; +} + +// Clear all settings (windows, tables, docking etc.) +void ImGui::ClearIniSettings() +{ + ImGuiContext& g = *GImGui; + g.SettingsIniData.clear(); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ClearAllFn != NULL) + handler.ClearAllFn(&g, &handler); +} + +void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) +{ + size_t file_data_size = 0; + char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size); + if (!file_data) + return; + if (file_data_size > 0) + LoadIniSettingsFromMemory(file_data, (size_t)file_data_size); + IM_FREE(file_data); +} + +// Zero-tolerance, no error reporting, cheap .ini parsing +// Set ini_size==0 to let us use strlen(ini_data). Do not call this function with a 0 if your buffer is actually empty! +void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + //IM_ASSERT(!g.WithinFrameScope && "Cannot be called between NewFrame() and EndFrame()"); + //IM_ASSERT(g.SettingsLoaded == false && g.FrameCount == 0); + + // For user convenience, we allow passing a non zero-terminated string (hence the ini_size parameter). + // For our convenience and to make the code simpler, we'll also write zero-terminators within the buffer. So let's create a writable copy.. + if (ini_size == 0) + ini_size = strlen(ini_data); + g.SettingsIniData.Buf.resize((int)ini_size + 1); + char* const buf = g.SettingsIniData.Buf.Data; + char* const buf_end = buf + ini_size; + memcpy(buf, ini_data, ini_size); + buf_end[0] = 0; + + // Call pre-read handlers + // Some types will clear their data (e.g. dock information) some types will allow merge/override (window) + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ReadInitFn != NULL) + handler.ReadInitFn(&g, &handler); + + void* entry_data = NULL; + ImGuiSettingsHandler* entry_handler = NULL; + + char* line_end = NULL; + for (char* line = buf; line < buf_end; line = line_end + 1) + { + // Skip new lines markers, then find end of the line + while (*line == '\n' || *line == '\r') + line++; + line_end = line; + while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') + line_end++; + line_end[0] = 0; + if (line[0] == ';') + continue; + if (line[0] == '[' && line_end > line && line_end[-1] == ']') + { + // Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code. + line_end[-1] = 0; + const char* name_end = line_end - 1; + const char* type_start = line + 1; + char* type_end = (char*)(void*)ImStrchrRange(type_start, name_end, ']'); + const char* name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; + if (!type_end || !name_start) + continue; + *type_end = 0; // Overwrite first ']' + name_start++; // Skip second '[' + entry_handler = FindSettingsHandler(type_start); + entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; + } + else if (entry_handler != NULL && entry_data != NULL) + { + // Let type handler parse the line + entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); + } + } + g.SettingsLoaded = true; + + // [DEBUG] Restore untouched copy so it can be browsed in Metrics (not strictly necessary) + memcpy(buf, ini_data, ini_size); + + // Call post-read handlers + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ApplyAllFn != NULL) + handler.ApplyAllFn(&g, &handler); +} + +void ImGui::SaveIniSettingsToDisk(const char* ini_filename) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + if (!ini_filename) + return; + + size_t ini_data_size = 0; + const char* ini_data = SaveIniSettingsToMemory(&ini_data_size); + ImFileHandle f = ImFileOpen(ini_filename, "wt"); + if (!f) + return; + ImFileWrite(ini_data, sizeof(char), ini_data_size, f); + ImFileClose(f); +} + +// Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer +const char* ImGui::SaveIniSettingsToMemory(size_t* out_size) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + g.SettingsIniData.Buf.resize(0); + g.SettingsIniData.Buf.push_back(0); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + handler.WriteAllFn(&g, &handler, &g.SettingsIniData); + if (out_size) + *out_size = (size_t)g.SettingsIniData.size(); + return g.SettingsIniData.c_str(); +} + +ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) +{ + ImGuiContext& g = *GImGui; + + if (g.IO.ConfigDebugIniSettings == false) + { + // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier. + if (const char* p = strstr(name, "###")) + name = p; + } + const size_t name_len = strlen(name); + + // Allocate chunk + const size_t chunk_size = sizeof(ImGuiWindowSettings) + name_len + 1; + ImGuiWindowSettings* settings = g.SettingsWindows.alloc_chunk(chunk_size); + IM_PLACEMENT_NEW(settings) ImGuiWindowSettings(); + settings->ID = ImHashStr(name, name_len); + memcpy(settings->GetName(), name, name_len + 1); // Store with zero terminator + + return settings; +} + +// We don't provide a FindWindowSettingsByName() because Docking system doesn't always hold on names. +// This is called once per window .ini entry + once per newly instantiated window. +ImGuiWindowSettings* ImGui::FindWindowSettingsByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->ID == id && !settings->WantDelete) + return settings; + return NULL; +} + +// This is faster if you are holding on a Window already as we don't need to perform a search. +ImGuiWindowSettings* ImGui::FindWindowSettingsByWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (window->SettingsOffset != -1) + return g.SettingsWindows.ptr_from_offset(window->SettingsOffset); + return FindWindowSettingsByID(window->ID); +} + +// This will revert window to its initial state, including enabling the ImGuiCond_FirstUseEver/ImGuiCond_Once conditions once more. +void ImGui::ClearWindowSettings(const char* name) +{ + //IMGUI_DEBUG_LOG("ClearWindowSettings('%s')\n", name); + ImGuiWindow* window = FindWindowByName(name); + if (window != NULL) + { + window->Flags |= ImGuiWindowFlags_NoSavedSettings; + InitOrLoadWindowSettings(window, NULL); + } + if (ImGuiWindowSettings* settings = window ? FindWindowSettingsByWindow(window) : FindWindowSettingsByID(ImHashStr(name))) + settings->WantDelete = true; +} + +static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + window->SettingsOffset = -1; + g.SettingsWindows.clear(); +} + +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = ImHashStr(name); + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByID(id); + if (settings) + *settings = ImGuiWindowSettings(); // Clear existing if recycling previous entry + else + settings = ImGui::CreateNewWindowSettings(name); + settings->ID = id; + settings->WantApply = true; + return (void*)settings; +} + +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; + int x, y; + int i; + if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } + else if (sscanf(line, "IsChild=%d", &i) == 1) { settings->IsChild = (i != 0); } +} + +// Apply to existing windows (if any) +static void WindowSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->WantApply) + { + if (ImGuiWindow* window = ImGui::FindWindowByID(settings->ID)) + ApplyWindowSettings(window, settings); + settings->WantApply = false; + } +} + +static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + // Gather data from windows that were active during this session + // (if a window wasn't opened in this session we preserve its settings) + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + { + if (window->Flags & ImGuiWindowFlags_NoSavedSettings) + continue; + + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByWindow(window); + if (!settings) + { + settings = ImGui::CreateNewWindowSettings(window->Name); + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + } + IM_ASSERT(settings->ID == window->ID); + settings->Pos = ImVec2ih(window->Pos); + settings->Size = ImVec2ih(window->SizeFull); + settings->IsChild = (window->Flags & ImGuiWindowFlags_ChildWindow) != 0; + settings->Collapsed = window->Collapsed; + settings->WantDelete = false; + } + + // Write to text buffer + buf->reserve(buf->size() + g.SettingsWindows.size() * 6); // ballpark reserve + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + if (settings->WantDelete) + continue; + const char* settings_name = settings->GetName(); + buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); + if (settings->IsChild) + { + buf->appendf("IsChild=1\n"); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + } + else + { + buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + if (settings->Collapsed) + buf->appendf("Collapsed=1\n"); + } + buf->append("\n"); + } +} + + +//----------------------------------------------------------------------------- +// [SECTION] LOCALIZATION +//----------------------------------------------------------------------------- + +void ImGui::LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count) +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < count; n++) + g.LocalizationTable[entries[n].Key] = entries[n].Text; +} + + +//----------------------------------------------------------------------------- +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +//----------------------------------------------------------------------------- +// - GetMainViewport() +// - SetWindowViewport() [Internal] +// - UpdateViewportsNewFrame() [Internal] +// (this section is more complete in the 'docking' branch) +//----------------------------------------------------------------------------- + +ImGuiViewport* ImGui::GetMainViewport() +{ + ImGuiContext& g = *GImGui; + return g.Viewports[0]; +} + +void ImGui::SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + window->Viewport = viewport; +} + +// Update viewports and monitor infos +static void ImGui::UpdateViewportsNewFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Viewports.Size == 1); + + // Update main viewport with current platform position. + // FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent. + ImGuiViewportP* main_viewport = g.Viewports[0]; + main_viewport->Flags = ImGuiViewportFlags_IsPlatformWindow | ImGuiViewportFlags_OwnedByApp; + main_viewport->Pos = ImVec2(0.0f, 0.0f); + main_viewport->Size = g.IO.DisplaySize; + + for (ImGuiViewportP* viewport : g.Viewports) + { + // Lock down space taken by menu bars and status bars + // Setup initial value for functions like BeginMainMenuBar(), DockSpaceOverViewport() etc. + viewport->WorkInsetMin = viewport->BuildWorkInsetMin; + viewport->WorkInsetMax = viewport->BuildWorkInsetMax; + viewport->BuildWorkInsetMin = viewport->BuildWorkInsetMax = ImVec2(0.0f, 0.0f); + viewport->UpdateWorkRect(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DOCKING +//----------------------------------------------------------------------------- + +// (this section is filled in the 'docking' branch) + + +//----------------------------------------------------------------------------- +// [SECTION] PLATFORM DEPENDENT HELPERS +//----------------------------------------------------------------------------- +// - Default clipboard handlers +// - Default shell function handlers +// - Default IME handlers +//----------------------------------------------------------------------------- + +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) + +#ifdef _MSC_VER +#pragma comment(lib, "user32") +#pragma comment(lib, "kernel32") +#endif + +// Win32 clipboard implementation +// We use g.ClipboardHandlerData for temporary storage to ensure it is freed on Shutdown() +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); + if (!::OpenClipboard(NULL)) + return NULL; + HANDLE wbuf_handle = ::GetClipboardData(CF_UNICODETEXT); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return NULL; + } + if (const WCHAR* wbuf_global = (const WCHAR*)::GlobalLock(wbuf_handle)) + { + int buf_len = ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, NULL, 0, NULL, NULL); + g.ClipboardHandlerData.resize(buf_len); + ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, g.ClipboardHandlerData.Data, buf_len, NULL, NULL); + } + ::GlobalUnlock(wbuf_handle); + ::CloseClipboard(); + return g.ClipboardHandlerData.Data; +} + +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) +{ + if (!::OpenClipboard(NULL)) + return; + const int wbuf_length = ::MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); + HGLOBAL wbuf_handle = ::GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(WCHAR)); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return; + } + WCHAR* wbuf_global = (WCHAR*)::GlobalLock(wbuf_handle); + ::MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf_global, wbuf_length); + ::GlobalUnlock(wbuf_handle); + ::EmptyClipboard(); + if (::SetClipboardData(CF_UNICODETEXT, wbuf_handle) == NULL) + ::GlobalFree(wbuf_handle); + ::CloseClipboard(); +} + +#elif defined(__APPLE__) && TARGET_OS_OSX && defined(IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS) + +#include // Use old API to avoid need for separate .mm file +static PasteboardRef main_clipboard = 0; + +// OSX clipboard implementation +// If you enable this you will need to add '-framework ApplicationServices' to your linker command-line! +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) +{ + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardClear(main_clipboard); + CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)text, strlen(text)); + if (cf_data) + { + PasteboardPutItemFlavor(main_clipboard, (PasteboardItemID)1, CFSTR("public.utf8-plain-text"), cf_data, 0); + CFRelease(cf_data); + } +} + +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardSynchronize(main_clipboard); + + ItemCount item_count = 0; + PasteboardGetItemCount(main_clipboard, &item_count); + for (ItemCount i = 0; i < item_count; i++) + { + PasteboardItemID item_id = 0; + PasteboardGetItemIdentifier(main_clipboard, i + 1, &item_id); + CFArrayRef flavor_type_array = 0; + PasteboardCopyItemFlavors(main_clipboard, item_id, &flavor_type_array); + for (CFIndex j = 0, nj = CFArrayGetCount(flavor_type_array); j < nj; j++) + { + CFDataRef cf_data; + if (PasteboardCopyItemFlavorData(main_clipboard, item_id, CFSTR("public.utf8-plain-text"), &cf_data) == noErr) + { + g.ClipboardHandlerData.clear(); + int length = (int)CFDataGetLength(cf_data); + g.ClipboardHandlerData.resize(length + 1); + CFDataGetBytes(cf_data, CFRangeMake(0, length), (UInt8*)g.ClipboardHandlerData.Data); + g.ClipboardHandlerData[length] = 0; + CFRelease(cf_data); + return g.ClipboardHandlerData.Data; + } + } + } + return NULL; +} + +#else + +// Local Dear ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers. +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + return g.ClipboardHandlerData.empty() ? NULL : g.ClipboardHandlerData.begin(); +} + +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text) +{ + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); + const char* text_end = text + strlen(text); + g.ClipboardHandlerData.resize((int)(text_end - text) + 1); + memcpy(&g.ClipboardHandlerData[0], text, (size_t)(text_end - text)); + g.ClipboardHandlerData[(int)(text_end - text)] = 0; +} + +#endif // Default clipboard handlers + +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#if defined(__APPLE__) && TARGET_OS_IPHONE +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif + +#if defined(_WIN32) && defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#endif + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#ifdef _WIN32 +#include // ShellExecuteA() +#ifdef _MSC_VER +#pragma comment(lib, "shell32") +#endif +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ + return (INT_PTR)::ShellExecuteA(NULL, "open", path, NULL, NULL, SW_SHOWDEFAULT) > 32; +} +#else +#include +#include +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ +#if defined(__APPLE__) + const char* args[] { "open", "--", path, NULL }; +#else + const char* args[] { "xdg-open", path, NULL }; +#endif + pid_t pid = fork(); + if (pid < 0) + return false; + if (!pid) + { + execvp(args[0], const_cast(args)); + exit(-1); + } + else + { + int status; + waitpid(pid, &status, 0); + return WEXITSTATUS(status) == 0; + } +} +#endif +#else +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char*) { return false; } +#endif // Default shell handlers + +//----------------------------------------------------------------------------- + +// Win32 API IME support (for Asian languages, etc.) +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) + +#include +#ifdef _MSC_VER +#pragma comment(lib, "imm32") +#endif + +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport* viewport, ImGuiPlatformImeData* data) +{ + // Notify OS Input Method Editor of text input position + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + if (hwnd == 0) + return; + + //::ImmAssociateContextEx(hwnd, NULL, data->WantVisible ? IACE_DEFAULT : 0); + if (HIMC himc = ::ImmGetContext(hwnd)) + { + COMPOSITIONFORM composition_form = {}; + composition_form.ptCurrentPos.x = (LONG)data->InputPos.x; + composition_form.ptCurrentPos.y = (LONG)data->InputPos.y; + composition_form.dwStyle = CFS_FORCE_POSITION; + ::ImmSetCompositionWindow(himc, &composition_form); + CANDIDATEFORM candidate_form = {}; + candidate_form.dwStyle = CFS_CANDIDATEPOS; + candidate_form.ptCurrentPos.x = (LONG)data->InputPos.x; + candidate_form.ptCurrentPos.y = (LONG)data->InputPos.y; + ::ImmSetCandidateWindow(himc, &candidate_form); + ::ImmReleaseContext(hwnd, himc); + } +} + +#else + +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport*, ImGuiPlatformImeData*) {} + +#endif // Default IME handlers + +//----------------------------------------------------------------------------- +// [SECTION] METRICS/DEBUGGER WINDOW +//----------------------------------------------------------------------------- +// - DebugRenderViewportThumbnail() [Internal] +// - RenderViewportsThumbnails() [Internal] +// - DebugTextEncoding() +// - MetricsHelpMarker() [Internal] +// - ShowFontAtlas() [Internal] +// - ShowMetricsWindow() +// - DebugNodeColumns() [Internal] +// - DebugNodeDrawList() [Internal] +// - DebugNodeDrawCmdShowMeshAndBoundingBox() [Internal] +// - DebugNodeFont() [Internal] +// - DebugNodeFontGlyph() [Internal] +// - DebugNodeStorage() [Internal] +// - DebugNodeTabBar() [Internal] +// - DebugNodeViewport() [Internal] +// - DebugNodeWindow() [Internal] +// - DebugNodeWindowSettings() [Internal] +// - DebugNodeWindowsList() [Internal] +// - DebugNodeWindowsListByBeginStackParent() [Internal] +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + +void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImVec2 scale = bb.GetSize() / viewport->Size; + ImVec2 off = bb.Min - viewport->Pos * scale; + float alpha_mul = 1.0f; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul * 0.40f)); + for (ImGuiWindow* thumb_window : g.Windows) + { + if (!thumb_window->WasActive || (thumb_window->Flags & ImGuiWindowFlags_ChildWindow)) + continue; + + ImRect thumb_r = thumb_window->Rect(); + ImRect title_r = thumb_window->TitleBarRect(); + thumb_r = ImRect(ImTrunc(off + thumb_r.Min * scale), ImTrunc(off + thumb_r.Max * scale)); + title_r = ImRect(ImTrunc(off + title_r.Min * scale), ImTrunc(off + ImVec2(title_r.Max.x, title_r.Min.y + title_r.GetHeight() * 3.0f) * scale)); // Exaggerate title bar height + thumb_r.ClipWithFull(bb); + title_r.ClipWithFull(bb); + const bool window_is_focused = (g.NavWindow && thumb_window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + window->DrawList->AddRectFilled(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_WindowBg, alpha_mul)); + window->DrawList->AddRectFilled(title_r.Min, title_r.Max, GetColorU32(window_is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg, alpha_mul)); + window->DrawList->AddRect(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + window->DrawList->AddText(g.Font, g.FontSize * 1.0f, title_r.Min, GetColorU32(ImGuiCol_Text, alpha_mul), thumb_window->Name, FindRenderedTextEnd(thumb_window->Name)); + } + draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + if (viewport->ID == g.DebugMetricsConfig.HighlightViewportID) + window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); +} + +static void RenderViewportsThumbnails() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + float SCALE = 1.0f / 8.0f; + ImRect bb_full(g.Viewports[0]->Pos, g.Viewports[0]->Pos + g.Viewports[0]->Size); + ImVec2 p = window->DC.CursorPos; + ImVec2 off = p - bb_full.Min * SCALE; + + // Draw viewports + for (ImGuiViewportP* viewport : g.Viewports) + { + ImRect viewport_draw_bb(off + (viewport->Pos) * SCALE, off + (viewport->Pos + viewport->Size) * SCALE); + ImGui::DebugRenderViewportThumbnail(window->DrawList, viewport, viewport_draw_bb); + } + ImGui::Dummy(bb_full.GetSize() * SCALE); +} + +// Draw an arbitrary US keyboard layout to visualize translated keys +void ImGui::DebugRenderKeyboardPreview(ImDrawList* draw_list) +{ + const float scale = ImGui::GetFontSize() / 13.0f; + const ImVec2 key_size = ImVec2(35.0f, 35.0f) * scale; + const float key_rounding = 3.0f * scale; + const ImVec2 key_face_size = ImVec2(25.0f, 25.0f) * scale; + const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f) * scale; + const float key_face_rounding = 2.0f * scale; + const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f) * scale; + const ImVec2 key_step = ImVec2(key_size.x - 1.0f, key_size.y - 1.0f); + const float key_row_offset = 9.0f * scale; + + ImVec2 board_min = GetCursorScreenPos(); + ImVec2 board_max = ImVec2(board_min.x + 3 * key_step.x + 2 * key_row_offset + 10.0f, board_min.y + 3 * key_step.y + 10.0f); + ImVec2 start_pos = ImVec2(board_min.x + 5.0f - key_step.x, board_min.y); + + struct KeyLayoutData { int Row, Col; const char* Label; ImGuiKey Key; }; + const KeyLayoutData keys_to_display[] = + { + { 0, 0, "", ImGuiKey_Tab }, { 0, 1, "Q", ImGuiKey_Q }, { 0, 2, "W", ImGuiKey_W }, { 0, 3, "E", ImGuiKey_E }, { 0, 4, "R", ImGuiKey_R }, + { 1, 0, "", ImGuiKey_CapsLock }, { 1, 1, "A", ImGuiKey_A }, { 1, 2, "S", ImGuiKey_S }, { 1, 3, "D", ImGuiKey_D }, { 1, 4, "F", ImGuiKey_F }, + { 2, 0, "", ImGuiKey_LeftShift },{ 2, 1, "Z", ImGuiKey_Z }, { 2, 2, "X", ImGuiKey_X }, { 2, 3, "C", ImGuiKey_C }, { 2, 4, "V", ImGuiKey_V } + }; + + // Elements rendered manually via ImDrawList API are not clipped automatically. + // While not strictly necessary, here IsItemVisible() is used to avoid rendering these shapes when they are out of view. + Dummy(board_max - board_min); + if (!IsItemVisible()) + return; + draw_list->PushClipRect(board_min, board_max, true); + for (int n = 0; n < IM_ARRAYSIZE(keys_to_display); n++) + { + const KeyLayoutData* key_data = &keys_to_display[n]; + ImVec2 key_min = ImVec2(start_pos.x + key_data->Col * key_step.x + key_data->Row * key_row_offset, start_pos.y + key_data->Row * key_step.y); + ImVec2 key_max = key_min + key_size; + draw_list->AddRectFilled(key_min, key_max, IM_COL32(204, 204, 204, 255), key_rounding); + draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding); + ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y); + ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y); + draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, ImDrawFlags_None, 2.0f); + draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding); + ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y); + draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label); + if (IsKeyDown(key_data->Key)) + draw_list->AddRectFilled(key_min, key_max, IM_COL32(255, 0, 0, 128), key_rounding); + } + draw_list->PopClipRect(); +} + +// Helper tool to diagnose between text encoding issues and font loading issues. Pass your UTF-8 string and verify that there are correct. +void ImGui::DebugTextEncoding(const char* str) +{ + Text("Text: \"%s\"", str); + if (!BeginTable("##DebugTextEncoding", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable)) + return; + TableSetupColumn("Offset"); + TableSetupColumn("UTF-8"); + TableSetupColumn("Glyph"); + TableSetupColumn("Codepoint"); + TableHeadersRow(); + for (const char* p = str; *p != 0; ) + { + unsigned int c; + const int c_utf8_len = ImTextCharFromUtf8(&c, p, NULL); + TableNextColumn(); + Text("%d", (int)(p - str)); + TableNextColumn(); + for (int byte_index = 0; byte_index < c_utf8_len; byte_index++) + { + if (byte_index > 0) + SameLine(); + Text("0x%02X", (int)(unsigned char)p[byte_index]); + } + TableNextColumn(); + if (GetFont()->FindGlyphNoFallback((ImWchar)c)) + TextUnformatted(p, p + c_utf8_len); + else + TextUnformatted((c == IM_UNICODE_CODEPOINT_INVALID) ? "[invalid]" : "[missing]"); + TableNextColumn(); + Text("U+%04X", (int)c); + p += c_utf8_len; + } + EndTable(); +} + +static void DebugFlashStyleColorStop() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorIdx != ImGuiCol_COUNT) + g.Style.Colors[g.DebugFlashStyleColorIdx] = g.DebugFlashStyleColorBackup; + g.DebugFlashStyleColorIdx = ImGuiCol_COUNT; +} + +// Flash a given style color for some + inhibit modifications of this color via PushStyleColor() calls. +void ImGui::DebugFlashStyleColor(ImGuiCol idx) +{ + ImGuiContext& g = *GImGui; + DebugFlashStyleColorStop(); + g.DebugFlashStyleColorTime = 0.5f; + g.DebugFlashStyleColorIdx = idx; + g.DebugFlashStyleColorBackup = g.Style.Colors[idx]; +} + +void ImGui::UpdateDebugToolFlashStyleColor() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorTime <= 0.0f) + return; + ColorConvertHSVtoRGB(ImCos(g.DebugFlashStyleColorTime * 6.0f) * 0.5f + 0.5f, 0.5f, 0.5f, g.Style.Colors[g.DebugFlashStyleColorIdx].x, g.Style.Colors[g.DebugFlashStyleColorIdx].y, g.Style.Colors[g.DebugFlashStyleColorIdx].z); + g.Style.Colors[g.DebugFlashStyleColorIdx].w = 1.0f; + if ((g.DebugFlashStyleColorTime -= g.IO.DeltaTime) <= 0.0f) + DebugFlashStyleColorStop(); +} + +// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds. +static void MetricsHelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +// [DEBUG] List fonts in a font atlas and display its texture +void ImGui::ShowFontAtlas(ImFontAtlas* atlas) +{ + for (ImFont* font : atlas->Fonts) + { + PushID(font); + DebugNodeFont(font); + PopID(); + } + if (TreeNode("Font Atlas", "Font Atlas (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight)) + { + ImGuiContext& g = *GImGui; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + Checkbox("Tint with Text Color", &cfg->ShowAtlasTintedWithTextColor); // Using text color ensure visibility of core atlas data, but will alter custom colored icons + ImVec4 tint_col = cfg->ShowAtlasTintedWithTextColor ? GetStyleColorVec4(ImGuiCol_Text) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + ImVec4 border_col = GetStyleColorVec4(ImGuiCol_Border); + Image(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), tint_col, border_col); + TreePop(); + } +} + +void ImGui::ShowMetricsWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + if (cfg->ShowDebugLog) + ShowDebugLogWindow(&cfg->ShowDebugLog); + if (cfg->ShowIDStackTool) + ShowIDStackToolWindow(&cfg->ShowIDStackTool); + + if (!Begin("Dear ImGui Metrics/Debugger", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // [DEBUG] Clear debug breaks hooks after exactly one cycle. + DebugBreakClearData(); + + // Basic info + Text("Dear ImGui %s", GetVersion()); + if (g.ContextName[0] != 0) + { + SameLine(); + Text("(Context Name: \"%s\")", g.ContextName); + } + Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); + Text("%d visible windows, %d current allocations", io.MetricsRenderWindows, g.DebugAllocInfo.TotalAllocCount - g.DebugAllocInfo.TotalFreeCount); + //SameLine(); if (SmallButton("GC")) { g.GcCompactAll = true; } + + Separator(); + + // Debugging enums + enum { WRT_OuterRect, WRT_OuterRectClipped, WRT_InnerRect, WRT_InnerClipRect, WRT_WorkRect, WRT_Content, WRT_ContentIdeal, WRT_ContentRegionRect, WRT_Count }; // Windows Rect Type + const char* wrt_rects_names[WRT_Count] = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Content", "ContentIdeal", "ContentRegionRect" }; + enum { TRT_OuterRect, TRT_InnerRect, TRT_WorkRect, TRT_HostClipRect, TRT_InnerClipRect, TRT_BackgroundClipRect, TRT_ColumnsRect, TRT_ColumnsWorkRect, TRT_ColumnsClipRect, TRT_ColumnsContentHeadersUsed, TRT_ColumnsContentHeadersIdeal, TRT_ColumnsContentFrozen, TRT_ColumnsContentUnfrozen, TRT_Count }; // Tables Rect Type + const char* trt_rects_names[TRT_Count] = { "OuterRect", "InnerRect", "WorkRect", "HostClipRect", "InnerClipRect", "BackgroundClipRect", "ColumnsRect", "ColumnsWorkRect", "ColumnsClipRect", "ColumnsContentHeadersUsed", "ColumnsContentHeadersIdeal", "ColumnsContentFrozen", "ColumnsContentUnfrozen" }; + if (cfg->ShowWindowsRectsType < 0) + cfg->ShowWindowsRectsType = WRT_WorkRect; + if (cfg->ShowTablesRectsType < 0) + cfg->ShowTablesRectsType = TRT_WorkRect; + + struct Funcs + { + static ImRect GetTableRect(ImGuiTable* table, int rect_type, int n) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); // Always using last submitted instance + if (rect_type == TRT_OuterRect) { return table->OuterRect; } + else if (rect_type == TRT_InnerRect) { return table->InnerRect; } + else if (rect_type == TRT_WorkRect) { return table->WorkRect; } + else if (rect_type == TRT_HostClipRect) { return table->HostClipRect; } + else if (rect_type == TRT_InnerClipRect) { return table->InnerClipRect; } + else if (rect_type == TRT_BackgroundClipRect) { return table->BgClipRect; } + else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table_instance->LastOuterHeight); } + else if (rect_type == TRT_ColumnsWorkRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->WorkRect.Min.y, c->WorkMaxX, table->WorkRect.Max.y); } + else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } + else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } + else if (rect_type == TRT_ColumnsContentFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXFrozen, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight); } + else if (rect_type == TRT_ColumnsContentUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight, c->ContentMaxXUnfrozen, table->InnerClipRect.Max.y); } + IM_ASSERT(0); + return ImRect(); + } + + static ImRect GetWindowRect(ImGuiWindow* window, int rect_type) + { + if (rect_type == WRT_OuterRect) { return window->Rect(); } + else if (rect_type == WRT_OuterRectClipped) { return window->OuterRectClipped; } + else if (rect_type == WRT_InnerRect) { return window->InnerRect; } + else if (rect_type == WRT_InnerClipRect) { return window->InnerClipRect; } + else if (rect_type == WRT_WorkRect) { return window->WorkRect; } + else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } + else if (rect_type == WRT_ContentIdeal) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSizeIdeal); } + else if (rect_type == WRT_ContentRegionRect) { return window->ContentRegionRect; } + IM_ASSERT(0); + return ImRect(); + } + }; + + // Tools + if (TreeNode("Tools")) + { + // Debug Break features + // The Item Picker tool is super useful to visually select an item and break into the call-stack of where it was submitted. + SeparatorTextEx(0, "Debug breaks", NULL, CalcTextSize("(?)").x + g.Style.SeparatorTextPadding.x); + SameLine(); + MetricsHelpMarker("Will call the IM_DEBUG_BREAK() macro to break in debugger.\nWarning: If you don't have a debugger attached, this will probably crash."); + if (Checkbox("Show Item Picker", &g.DebugItemPickerActive) && g.DebugItemPickerActive) + DebugStartItemPicker(); + Checkbox("Show \"Debug Break\" buttons in other sections (io.ConfigDebugIsDebuggerPresent)", &g.IO.ConfigDebugIsDebuggerPresent); + + SeparatorText("Visualize"); + + Checkbox("Show Debug Log", &cfg->ShowDebugLog); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowDebugLogWindow() from your code."); + + Checkbox("Show ID Stack Tool", &cfg->ShowIDStackTool); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowIDStackToolWindow() from your code."); + + Checkbox("Show windows begin order", &cfg->ShowWindowsBeginOrder); + Checkbox("Show windows rectangles", &cfg->ShowWindowsRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowWindowsRects |= Combo("##show_windows_rect_type", &cfg->ShowWindowsRectsType, wrt_rects_names, WRT_Count, WRT_Count); + if (cfg->ShowWindowsRects && g.NavWindow != NULL) + { + BulletText("'%s':", g.NavWindow->Name); + Indent(); + for (int rect_n = 0; rect_n < WRT_Count; rect_n++) + { + ImRect r = Funcs::GetWindowRect(g.NavWindow, rect_n); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), wrt_rects_names[rect_n]); + } + Unindent(); + } + + Checkbox("Show tables rectangles", &cfg->ShowTablesRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowTablesRects |= Combo("##show_table_rects_type", &cfg->ShowTablesRectsType, trt_rects_names, TRT_Count, TRT_Count); + if (cfg->ShowTablesRects && g.NavWindow != NULL) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1 || (table->OuterWindow != g.NavWindow && table->InnerWindow != g.NavWindow)) + continue; + + BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + Indent(); + char buf[128]; + for (int rect_n = 0; rect_n < TRT_Count; rect_n++) + { + if (rect_n >= TRT_ColumnsRect) + { + if (rect_n != TRT_ColumnsRect && rect_n != TRT_ColumnsClipRect) + continue; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, rect_n, column_n); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, rect_n, -1); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + Unindent(); + } + } + Checkbox("Show groups rectangles", &g.DebugShowGroupRects); // Storing in context as this is used by group code and prefers to be in hot-data + + SeparatorText("Validate"); + + Checkbox("Debug Begin/BeginChild return value", &io.ConfigDebugBeginReturnValueLoop); + SameLine(); + MetricsHelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); + + Checkbox("UTF-8 Encoding viewer", &cfg->ShowTextEncodingViewer); + SameLine(); + MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct."); + if (cfg->ShowTextEncodingViewer) + { + static char buf[64] = ""; + SetNextItemWidth(-FLT_MIN); + InputText("##DebugTextEncodingBuf", buf, IM_ARRAYSIZE(buf)); + if (buf[0] != 0) + DebugTextEncoding(buf); + } + + TreePop(); + } + + // Windows + if (TreeNode("Windows", "Windows (%d)", g.Windows.Size)) + { + //SetNextItemOpen(true, ImGuiCond_Once); + DebugNodeWindowsList(&g.Windows, "By display order"); + DebugNodeWindowsList(&g.WindowsFocusOrder, "By focus order (root windows)"); + if (TreeNode("By submission order (begin stack)")) + { + // Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship! + ImVector& temp_buffer = g.WindowsTempSortBuffer; + temp_buffer.resize(0); + for (ImGuiWindow* window : g.Windows) + if (window->LastFrameActive + 1 >= g.FrameCount) + temp_buffer.push_back(window); + struct Func { static int IMGUI_CDECL WindowComparerByBeginOrder(const void* lhs, const void* rhs) { return ((int)(*(const ImGuiWindow* const *)lhs)->BeginOrderWithinContext - (*(const ImGuiWindow* const*)rhs)->BeginOrderWithinContext); } }; + ImQsort(temp_buffer.Data, (size_t)temp_buffer.Size, sizeof(ImGuiWindow*), Func::WindowComparerByBeginOrder); + DebugNodeWindowsListByBeginStackParent(temp_buffer.Data, temp_buffer.Size, NULL); + TreePop(); + } + + TreePop(); + } + + // DrawLists + int drawlist_count = 0; + for (ImGuiViewportP* viewport : g.Viewports) + drawlist_count += viewport->DrawDataP.CmdLists.Size; + if (TreeNode("DrawLists", "DrawLists (%d)", drawlist_count)) + { + Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); + Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); + for (ImGuiViewportP* viewport : g.Viewports) + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + TreePop(); + } + + // Viewports + if (TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size)) + { + SetNextItemOpen(true, ImGuiCond_Once); + if (TreeNode("Windows Minimap")) + { + RenderViewportsThumbnails(); + TreePop(); + } + cfg->HighlightViewportID = 0; + + for (ImGuiViewportP* viewport : g.Viewports) + DebugNodeViewport(viewport); + TreePop(); + } + + // Details for Popups + if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) + { + for (const ImGuiPopupData& popup_data : g.OpenPopupStack) + { + // As it's difficult to interact with tree nodes while popups are open, we display everything inline. + ImGuiWindow* window = popup_data.Window; + BulletText("PopupID: %08x, Window: '%s' (%s%s), RestoreNavWindow '%s', ParentWindow '%s'", + popup_data.PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", + popup_data.RestoreNavWindow ? popup_data.RestoreNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); + } + TreePop(); + } + + // Details for TabBars + if (TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.GetAliveCount())) + { + for (int n = 0; n < g.TabBars.GetMapSize(); n++) + if (ImGuiTabBar* tab_bar = g.TabBars.TryGetMapData(n)) + { + PushID(tab_bar); + DebugNodeTabBar(tab_bar, "TabBar"); + PopID(); + } + TreePop(); + } + + // Details for Tables + if (TreeNode("Tables", "Tables (%d)", g.Tables.GetAliveCount())) + { + for (int n = 0; n < g.Tables.GetMapSize(); n++) + if (ImGuiTable* table = g.Tables.TryGetMapData(n)) + DebugNodeTable(table); + TreePop(); + } + + // Details for Fonts + ImFontAtlas* atlas = g.IO.Fonts; + if (TreeNode("Fonts", "Fonts (%d)", atlas->Fonts.Size)) + { + ShowFontAtlas(atlas); + TreePop(); + } + + // Details for InputText + if (TreeNode("InputText")) + { + DebugNodeInputTextState(&g.InputTextState); + TreePop(); + } + + // Details for TypingSelect + if (TreeNode("TypingSelect", "TypingSelect (%d)", g.TypingSelectState.SearchBuffer[0] != 0 ? 1 : 0)) + { + DebugNodeTypingSelectState(&g.TypingSelectState); + TreePop(); + } + + // Details for MultiSelect + if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) + { + ImGuiBoxSelectState* bs = &g.BoxSelectState; + BulletText("BoxSelect ID=0x%08X, Starting = %d, Active %d", bs->ID, bs->IsStarting, bs->IsActive); + for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) + if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) + DebugNodeMultiSelectState(state); + TreePop(); + } + + // Details for Docking +#ifdef IMGUI_HAS_DOCK + if (TreeNode("Docking")) + { + TreePop(); + } +#endif // #ifdef IMGUI_HAS_DOCK + + // Settings + if (TreeNode("Settings")) + { + if (SmallButton("Clear")) + ClearIniSettings(); + SameLine(); + if (SmallButton("Save to memory")) + SaveIniSettingsToMemory(); + SameLine(); + if (SmallButton("Save to disk")) + SaveIniSettingsToDisk(g.IO.IniFilename); + SameLine(); + if (g.IO.IniFilename) + Text("\"%s\"", g.IO.IniFilename); + else + TextUnformatted(""); + Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); + if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) + { + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + BulletText("\"%s\"", handler.TypeName); + TreePop(); + } + if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + DebugNodeWindowSettings(settings); + TreePop(); + } + + if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) + { + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + DebugNodeTableSettings(settings); + TreePop(); + } + +#ifdef IMGUI_HAS_DOCK +#endif // #ifdef IMGUI_HAS_DOCK + + if (TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", g.SettingsIniData.size())) + { + InputTextMultiline("##Ini", (char*)(void*)g.SettingsIniData.c_str(), g.SettingsIniData.Buf.Size, ImVec2(-FLT_MIN, GetTextLineHeight() * 20), ImGuiInputTextFlags_ReadOnly); + TreePop(); + } + TreePop(); + } + + // Settings + if (TreeNode("Memory allocations")) + { + ImGuiDebugAllocInfo* info = &g.DebugAllocInfo; + Text("%d current allocations", info->TotalAllocCount - info->TotalFreeCount); + if (SmallButton("GC now")) { g.GcCompactAll = true; } + Text("Recent frames with allocations:"); + int buf_size = IM_ARRAYSIZE(info->LastEntriesBuf); + for (int n = buf_size - 1; n >= 0; n--) + { + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[(info->LastEntriesIdx - n + buf_size) % buf_size]; + BulletText("Frame %06d: %+3d ( %2d alloc, %2d free )", entry->FrameCount, entry->AllocCount - entry->FreeCount, entry->AllocCount, entry->FreeCount); + if (n == 0) + { + SameLine(); + Text("<- %d frames ago", g.FrameCount - entry->FrameCount); + } + } + TreePop(); + } + + if (TreeNode("Inputs")) + { + Text("KEYBOARD/GAMEPAD/MOUSE KEYS"); + { + // We iterate both legacy native range and named ImGuiKey ranges, which is a little odd but this allows displaying the data for old/new backends. + // User code should never have to go through such hoops! You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. + Indent(); +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey) { return false; } }; +#else + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey key) { return key >= 0 && key < 512 && GetIO().KeyMap[key] != -1; } }; // Hide Native<>ImGuiKey duplicates when both exists in the array + //Text("Legacy raw:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key++) { if (io.KeysDown[key]) { SameLine(); Text("\"%s\" %d", GetKeyName(key), key); } } +#endif + Text("Keys down:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !IsKeyDown(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); SameLine(); Text("(%.02f)", GetKeyData(key)->DownDuration); } + Text("Keys pressed:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !IsKeyPressed(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys released:"); for (ImGuiKey key = ImGuiKey_KeysData_OFFSET; key < ImGuiKey_COUNT; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !IsKeyReleased(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys mods: %s%s%s%s", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""); + Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; SameLine(); Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. + DebugRenderKeyboardPreview(GetWindowDrawList()); + Unindent(); + } + + Text("MOUSE STATE"); + { + Indent(); + if (IsMousePosValid()) + Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y); + else + Text("Mouse pos: "); + Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y); + int count = IM_ARRAYSIZE(io.MouseDown); + Text("Mouse down:"); for (int i = 0; i < count; i++) if (IsMouseDown(i)) { SameLine(); Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } + Text("Mouse clicked:"); for (int i = 0; i < count; i++) if (IsMouseClicked(i)) { SameLine(); Text("b%d (%d)", i, io.MouseClickedCount[i]); } + Text("Mouse released:"); for (int i = 0; i < count; i++) if (IsMouseReleased(i)) { SameLine(); Text("b%d", i); } + Text("Mouse wheel: %.1f", io.MouseWheel); + Text("MouseStationaryTimer: %.2f", g.MouseStationaryTimer); + Text("Mouse source: %s", GetMouseSourceName(io.MouseSource)); + Text("Pen Pressure: %.1f", io.PenPressure); // Note: currently unused + Unindent(); + } + + Text("MOUSE WHEELING"); + { + Indent(); + Text("WheelingWindow: '%s'", g.WheelingWindow ? g.WheelingWindow->Name : "NULL"); + Text("WheelingWindowReleaseTimer: %.2f", g.WheelingWindowReleaseTimer); + Text("WheelingAxisAvg[] = { %.3f, %.3f }, Main Axis: %s", g.WheelingAxisAvg.x, g.WheelingAxisAvg.y, (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? "X" : (g.WheelingAxisAvg.x < g.WheelingAxisAvg.y) ? "Y" : ""); + Unindent(); + } + + Text("KEY OWNERS"); + { + Indent(); + if (BeginChild("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + continue; + Text("%s: 0x%08X%s", GetKeyName(key), owner_data->OwnerCurr, + owner_data->LockUntilRelease ? " LockUntilRelease" : owner_data->LockThisFrame ? " LockThisFrame" : ""); + DebugLocateItemOnHover(owner_data->OwnerCurr); + } + EndChild(); + Unindent(); + } + Text("SHORTCUT ROUTING"); + SameLine(); + MetricsHelpMarker("Declared shortcut routes automatically set key owner when mods matches."); + { + Indent(); + if (BeginChild("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; ) + { + ImGuiKeyRoutingData* routing_data = &rt->Entries[idx]; + ImGuiKeyChord key_chord = key | routing_data->Mods; + Text("%s: 0x%08X (scored %d)", GetKeyChordName(key_chord), routing_data->RoutingCurr, routing_data->RoutingCurrScore); + DebugLocateItemOnHover(routing_data->RoutingCurr); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + SameLine(); + if (DebugBreakButton("**DebugBreak**", "in SetShortcutRouting() for this KeyChord")) + g.DebugBreakInShortcutRouting = key_chord; + } + idx = routing_data->NextEntryIndex; + } + } + EndChild(); + Text("(ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: 0x%X)", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Unindent(); + } + TreePop(); + } + + if (TreeNode("Internal state")) + { + Text("WINDOWING"); + Indent(); + Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); + Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindow->Name : "NULL"); + Text("HoveredWindowUnderMovingWindow: '%s'", g.HoveredWindowUnderMovingWindow ? g.HoveredWindowUnderMovingWindow->Name : "NULL"); + Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); + Unindent(); + + Text("ITEMS"); + Indent(); + Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d, Source: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, g.ActiveIdAllowOverlap, GetInputSourceName(g.ActiveIdSource)); + DebugLocateItemOnHover(g.ActiveId); + Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); + Text("ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: %X", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Not displaying g.HoveredId as it is update mid-frame + Text("HoverItemDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverItemDelayId, g.HoverItemDelayTimer, g.HoverItemDelayClearTimer); + Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); + DebugLocateItemOnHover(g.DragDropPayload.SourceId); + Unindent(); + + Text("NAV,FOCUS"); + Indent(); + Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); + Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); + DebugLocateItemOnHover(g.NavId); + Text("NavInputSource: %s", GetInputSourceName(g.NavInputSource)); + Text("NavLastValidSelectionUserData = %" IM_PRId64 " (0x%" IM_PRIX64 ")", g.NavLastValidSelectionUserData, g.NavLastValidSelectionUserData); + Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); + Text("NavActivateId/DownId/PressedId: %08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId, g.NavActivatePressedId); + Text("NavActivateFlags: %04X", g.NavActivateFlags); + Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); + Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId); + Text("NavFocusRoute[] = "); + for (int path_n = g.NavFocusRoute.Size - 1; path_n >= 0; path_n--) + { + const ImGuiFocusScopeData& focus_scope = g.NavFocusRoute[path_n]; + SameLine(0.0f, 0.0f); + Text("0x%08X/", focus_scope.ID); + SetItemTooltip("In window \"%s\"", FindWindowByID(focus_scope.WindowID)->Name); + } + Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); + Unindent(); + + TreePop(); + } + + // Overlay: Display windows Rectangles and Begin Order + if (cfg->ShowWindowsRects || cfg->ShowWindowsBeginOrder) + { + for (ImGuiWindow* window : g.Windows) + { + if (!window->WasActive) + continue; + ImDrawList* draw_list = GetForegroundDrawList(window); + if (cfg->ShowWindowsRects) + { + ImRect r = Funcs::GetWindowRect(window, cfg->ShowWindowsRectsType); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + if (cfg->ShowWindowsBeginOrder && !(window->Flags & ImGuiWindowFlags_ChildWindow)) + { + char buf[32]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "%d", window->BeginOrderWithinContext); + float font_size = GetFontSize(); + draw_list->AddRectFilled(window->Pos, window->Pos + ImVec2(font_size, font_size), IM_COL32(200, 100, 100, 255)); + draw_list->AddText(window->Pos, IM_COL32(255, 255, 255, 255), buf); + } + } + } + + // Overlay: Display Tables Rectangles + if (cfg->ShowTablesRects) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1) + continue; + ImDrawList* draw_list = GetForegroundDrawList(table->OuterWindow); + if (cfg->ShowTablesRectsType >= TRT_ColumnsRect) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); + ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); + float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; + draw_list->AddRect(r.Min, r.Max, col, 0.0f, 0, thickness); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, -1); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + } + } + +#ifdef IMGUI_HAS_DOCK + // Overlay: Display Docking info + if (show_docking_nodes && g.IO.KeyCtrl) + { + } +#endif // #ifdef IMGUI_HAS_DOCK + + End(); +} + +void ImGui::DebugBreakClearData() +{ + // Those fields are scattered in their respective subsystem to stay in hot-data locations + ImGuiContext& g = *GImGui; + g.DebugBreakInWindow = 0; + g.DebugBreakInTable = 0; + g.DebugBreakInShortcutRouting = ImGuiKey_None; +} + +void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location) +{ + if (!BeginItemTooltip()) + return; + Text("To call IM_DEBUG_BREAK() %s:", description_of_location); + Separator(); + TextUnformatted(keyboard_only ? "- Press 'Pause/Break' on keyboard." : "- Press 'Pause/Break' on keyboard.\n- or Click (may alter focus/active id).\n- or navigate using keyboard and press space."); + Separator(); + TextUnformatted("Choose one way that doesn't interfere with what you are trying to debug!\nYou need a debugger attached or this will crash!"); + EndTooltip(); +} + +// Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. +// In order to reduce interferences with the contents we are trying to debug into. +bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 pos = window->DC.CursorPos + ImVec2(0.0f, window->DC.CurrLineTextBaseOffset); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x * 2.0f, label_size.y); + + const ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + // WE DO NOT USE ButtonEx() or ButtonBehavior() in order to reduce our side-effects. + bool hovered = ItemHoverable(bb, id, g.CurrentItemFlags); + bool pressed = hovered && (IsKeyChordPressed(g.DebugBreakKeyChord) || IsMouseClicked(0) || g.NavActivateId == id); + DebugBreakButtonTooltip(false, description_of_location); + + ImVec4 col4f = GetStyleColorVec4(hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImVec4 hsv; + ColorConvertRGBtoHSV(col4f.x, col4f.y, col4f.z, hsv.x, hsv.y, hsv.z); + ColorConvertHSVtoRGB(hsv.x + 0.20f, hsv.y, hsv.z, col4f.x, col4f.y, col4f.z); + + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, GetColorU32(col4f), true, g.Style.FrameRounding); + RenderTextClipped(bb.Min, bb.Max, label, NULL, &label_size, g.Style.ButtonTextAlign, &bb); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +// [DEBUG] Display contents of Columns +void ImGui::DebugNodeColumns(ImGuiOldColumns* columns) +{ + if (!TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) + return; + BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); + for (ImGuiOldColumnData& column : columns->Columns) + BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", (int)columns->Columns.index_from_ptr(&column), column.OffsetNorm, GetColumnOffsetFromNorm(columns, column.OffsetNorm)); + TreePop(); +} + +static void FormatTextureIDForDebugDisplay(char* buf, int buf_size, ImTextureID tex_id) +{ + union { void* ptr; int integer; } tex_id_opaque; + memcpy(&tex_id_opaque, &tex_id, ImMin(sizeof(void*), sizeof(tex_id))); + if (sizeof(tex_id) >= sizeof(void*)) + ImFormatString(buf, buf_size, "0x%p", tex_id_opaque.ptr); + else + ImFormatString(buf, buf_size, "0x%04X", tex_id_opaque.integer); +} + +// [DEBUG] Display contents of ImDrawList +void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(viewport); // Used in docking branch + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + int cmd_count = draw_list->CmdBuffer.Size; + if (cmd_count > 0 && draw_list->CmdBuffer.back().ElemCount == 0 && draw_list->CmdBuffer.back().UserCallback == NULL) + cmd_count--; + bool node_open = TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, cmd_count); + if (draw_list == GetWindowDrawList()) + { + SameLine(); + TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) + if (node_open) + TreePop(); + return; + } + + ImDrawList* fg_draw_list = GetForegroundDrawList(window); // Render additional visuals into the top-most draw list + if (window && IsItemHovered() && fg_draw_list) + fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!node_open) + return; + + if (window && !window->WasActive) + TextDisabled("Warning: owning Window is inactive. This DrawList is not being rendered!"); + + for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.Data; pcmd < draw_list->CmdBuffer.Data + cmd_count; pcmd++) + { + if (pcmd->UserCallback) + { + BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); + continue; + } + + char texid_desc[20]; + FormatTextureIDForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TextureId); + char buf[300]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", + pcmd->ElemCount / 3, texid_desc, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + bool pcmd_node_open = TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); + if (IsItemHovered() && (cfg->ShowDrawCmdMesh || cfg->ShowDrawCmdBoundingBoxes) && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, cfg->ShowDrawCmdMesh, cfg->ShowDrawCmdBoundingBoxes); + if (!pcmd_node_open) + continue; + + // Calculate approximate coverage area (touched pixel count) + // This will be in pixels squared as long there's no post-scaling happening to the renderer output. + const ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; + const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + pcmd->VtxOffset; + float total_area = 0.0f; + for (unsigned int idx_n = pcmd->IdxOffset; idx_n < pcmd->IdxOffset + pcmd->ElemCount; ) + { + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos; + total_area += ImTriangleArea(triangle[0], triangle[1], triangle[2]); + } + + // Display vertex information summary. Hover to get all triangles drawn in wire-frame + ImFormatString(buf, IM_ARRAYSIZE(buf), "Mesh: ElemCount: %d, VtxOffset: +%d, IdxOffset: +%d, Area: ~%0.f px", pcmd->ElemCount, pcmd->VtxOffset, pcmd->IdxOffset, total_area); + Selectable(buf); + if (IsItemHovered() && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, true, false); + + // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. + ImGuiListClipper clipper; + clipper.Begin(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + while (clipper.Step()) + for (int prim = clipper.DisplayStart, idx_i = pcmd->IdxOffset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) + { + char* buf_p = buf, * buf_end = buf + IM_ARRAYSIZE(buf); + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_i++) + { + const ImDrawVert& v = vtx_buffer[idx_buffer ? idx_buffer[idx_i] : idx_i]; + triangle[n] = v.pos; + buf_p += ImFormatString(buf_p, buf_end - buf_p, "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", + (n == 0) ? "Vert:" : " ", idx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); + } + + Selectable(buf, false); + if (fg_draw_list && IsItemHovered()) + { + ImDrawListFlags backup_flags = fg_draw_list->Flags; + fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); + fg_draw_list->Flags = backup_flags; + } + } + TreePop(); + } + TreePop(); +} + +// [DEBUG] Display mesh/aabb of a ImDrawCmd +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb) +{ + IM_ASSERT(show_mesh || show_aabb); + + // Draw wire-frame version of all triangles + ImRect clip_rect = draw_cmd->ClipRect; + ImRect vtxs_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + ImDrawListFlags backup_flags = out_draw_list->Flags; + out_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + for (unsigned int idx_n = draw_cmd->IdxOffset, idx_end = draw_cmd->IdxOffset + draw_cmd->ElemCount; idx_n < idx_end; ) + { + ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; // We don't hold on those pointers past iterations as ->AddPolyline() may invalidate them if out_draw_list==draw_list + ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + draw_cmd->VtxOffset; + + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos)); + if (show_mesh) + out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); // In yellow: mesh triangles + } + // Draw bounding boxes + if (show_aabb) + { + out_draw_list->AddRect(ImTrunc(clip_rect.Min), ImTrunc(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU + out_draw_list->AddRect(ImTrunc(vtxs_rect.Min), ImTrunc(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles + } + out_draw_list->Flags = backup_flags; +} + +// [DEBUG] Display details for a single font, called by ShowStyleEditor(). +void ImGui::DebugNodeFont(ImFont* font) +{ + bool opened = TreeNode(font, "Font: \"%s\"\n%.2f px, %d glyphs, %d file(s)", + font->ConfigData ? font->ConfigData[0].Name : "", font->FontSize, font->Glyphs.Size, font->ConfigDataCount); + SameLine(); + if (SmallButton("Set as default")) + GetIO().FontDefault = font; + if (!opened) + return; + + // Display preview text + PushFont(font); + Text("The quick brown fox jumps over the lazy dog"); + PopFont(); + + // Display details + SetNextItemWidth(GetFontSize() * 8); + DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); + SameLine(); MetricsHelpMarker( + "Note that the default embedded font is NOT meant to be scaled.\n\n" + "Font are currently rendered into bitmaps at a given size at the time of building the atlas. " + "You may oversample them to get some flexibility with scaling. " + "You can also render at multiple sizes and select which one to use at runtime.\n\n" + "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)"); + Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); + char c_str[5]; + Text("Fallback character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->FallbackChar), font->FallbackChar); + Text("Ellipsis character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->EllipsisChar), font->EllipsisChar); + const int surface_sqrt = (int)ImSqrt((float)font->MetricsTotalSurface); + Text("Texture Area: about %d px ~%dx%d px", font->MetricsTotalSurface, surface_sqrt, surface_sqrt); + for (int config_i = 0; config_i < font->ConfigDataCount; config_i++) + if (font->ConfigData) + if (const ImFontConfig* cfg = &font->ConfigData[config_i]) + BulletText("Input %d: \'%s\', Oversample: (%d,%d), PixelSnapH: %d, Offset: (%.1f,%.1f)", + config_i, cfg->Name, cfg->OversampleH, cfg->OversampleV, cfg->PixelSnapH, cfg->GlyphOffset.x, cfg->GlyphOffset.y); + + // Display all glyphs of the fonts in separate pages of 256 characters + if (TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size)) + { + ImDrawList* draw_list = GetWindowDrawList(); + const ImU32 glyph_col = GetColorU32(ImGuiCol_Text); + const float cell_size = font->FontSize * 1; + const float cell_spacing = GetStyle().ItemSpacing.y; + for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256) + { + // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k) + // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT + // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here) + if (!(base & 4095) && font->IsGlyphRangeUnused(base, base + 4095)) + { + base += 4096 - 256; + continue; + } + + int count = 0; + for (unsigned int n = 0; n < 256; n++) + if (font->FindGlyphNoFallback((ImWchar)(base + n))) + count++; + if (count <= 0) + continue; + if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph")) + continue; + + // Draw a 16x16 grid of glyphs + ImVec2 base_pos = GetCursorScreenPos(); + for (unsigned int n = 0; n < 256; n++) + { + // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions + // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string. + ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing)); + ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size); + const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n)); + draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50)); + if (!glyph) + continue; + font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n)); + if (IsMouseHoveringRect(cell_p1, cell_p2) && BeginTooltip()) + { + DebugNodeFontGlyph(font, glyph); + EndTooltip(); + } + } + Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16)); + TreePop(); + } + TreePop(); + } + TreePop(); +} + +void ImGui::DebugNodeFontGlyph(ImFont*, const ImFontGlyph* glyph) +{ + Text("Codepoint: U+%04X", glyph->Codepoint); + Separator(); + Text("Visible: %d", glyph->Visible); + Text("AdvanceX: %.1f", glyph->AdvanceX); + Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1); + Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1); +} + +// [DEBUG] Display contents of ImGuiStorage +void ImGui::DebugNodeStorage(ImGuiStorage* storage, const char* label) +{ + if (!TreeNode(label, "%s: %d entries, %d bytes", label, storage->Data.Size, storage->Data.size_in_bytes())) + return; + for (const ImGuiStoragePair& p : storage->Data) + { + BulletText("Key 0x%08X Value { i: %d }", p.key, p.val_i); // Important: we currently don't store a type, real value may not be integer. + DebugLocateItemOnHover(p.key); + } + TreePop(); +} + +// [DEBUG] Display contents of ImGuiTabBar +void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) +{ + // Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings. + char buf[256]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + const bool is_active = (tab_bar->PrevFrameVisible >= GetFrameCount() - 2); + p += ImFormatString(p, buf_end - p, "%s 0x%08X (%d tabs)%s {", label, tab_bar->ID, tab_bar->Tabs.Size, is_active ? "" : " *Inactive*"); + for (int tab_n = 0; tab_n < ImMin(tab_bar->Tabs.Size, 3); tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + p += ImFormatString(p, buf_end - p, "%s'%s'", tab_n > 0 ? ", " : "", TabBarGetTabName(tab_bar, tab)); + } + p += ImFormatString(p, buf_end - p, (tab_bar->Tabs.Size > 3) ? " ... }" : " } "); + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(label, "%s", buf); + if (!is_active) { PopStyleColor(); } + if (is_active && IsItemHovered()) + { + ImDrawList* draw_list = GetForegroundDrawList(); + draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + } + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + PushID(tab); + if (SmallButton("<")) { TabBarQueueReorder(tab_bar, tab, -1); } SameLine(0, 2); + if (SmallButton(">")) { TabBarQueueReorder(tab_bar, tab, +1); } SameLine(); + Text("%02d%c Tab 0x%08X '%s' Offset: %.2f, Width: %.2f/%.2f", + tab_n, (tab->ID == tab_bar->SelectedTabId) ? '*' : ' ', tab->ID, TabBarGetTabName(tab_bar, tab), tab->Offset, tab->Width, tab->ContentWidth); + PopID(); + } + TreePop(); + } +} + +void ImGui::DebugNodeViewport(ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + SetNextItemOpen(true, ImGuiCond_Once); + bool open = TreeNode("viewport0", "Viewport #%d", 0); + if (IsItemHovered()) + g.DebugMetricsConfig.HighlightViewportID = viewport->ID; + if (open) + { + ImGuiWindowFlags flags = viewport->Flags; + BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Inset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f", + viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y, + viewport->WorkInsetMin.x, viewport->WorkInsetMin.y, viewport->WorkInsetMax.x, viewport->WorkInsetMax.y); + BulletText("Flags: 0x%04X =%s%s%s", viewport->Flags, + (flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", + (flags & ImGuiViewportFlags_IsPlatformMonitor) ? " IsPlatformMonitor" : "", + (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : ""); + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + TreePop(); + } +} + +void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) +{ + if (window == NULL) + { + BulletText("%s: NULL", label); + return; + } + + ImGuiContext& g = *GImGui; + const bool is_active = window->WasActive; + ImGuiTreeNodeFlags tree_node_flags = (window == g.NavWindow) ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + const bool open = TreeNodeEx(label, tree_node_flags, "%s '%s'%s", label, window->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered() && is_active) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!open) + return; + + if (window->MemoryCompacted) + TextDisabled("Note: some memory buffers have been compacted/freed."); + + if (g.IO.ConfigDebugIsDebuggerPresent && DebugBreakButton("**DebugBreak**", "in Begin()")) + g.DebugBreakInWindow = window->ID; + + ImGuiWindowFlags flags = window->Flags; + DebugNodeDrawList(window, window->Viewport, window->DrawList, "DrawList"); + BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y, window->ContentSizeIdeal.x, window->ContentSizeIdeal.y); + BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, + (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", + (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", + (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); + if (flags & ImGuiWindowFlags_ChildWindow) + BulletText("ChildFlags: 0x%08X (%s%s%s%s..)", window->ChildFlags, + (window->ChildFlags & ImGuiChildFlags_Borders) ? "Borders " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeX) ? "ResizeX " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeY) ? "ResizeY " : "", + (window->ChildFlags & ImGuiChildFlags_NavFlattened) ? "NavFlattened " : ""); + BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y, window->ScrollbarX ? "X" : "", window->ScrollbarY ? "Y" : ""); + BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); + BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + { + ImRect r = window->NavRectRel[layer]; + if (r.Min.x >= r.Max.y && r.Min.y >= r.Max.y) + BulletText("NavLastIds[%d]: 0x%08X", layer, window->NavLastIds[layer]); + else + BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window->NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y); + DebugLocateItemOnHover(window->NavLastIds[layer]); + } + const ImVec2* pr = window->NavPreferredScoringPosRel; + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. + BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); + if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } + if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } + if (window->ParentWindowForFocusRoute != NULL) { DebugNodeWindow(window->ParentWindowForFocusRoute, "ParentWindowForFocusRoute"); } + if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } + if (window->ColumnsStorage.Size > 0 && TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) + { + for (ImGuiOldColumns& columns : window->ColumnsStorage) + DebugNodeColumns(&columns); + TreePop(); + } + DebugNodeStorage(&window->StateStorage, "Storage"); + TreePop(); +} + +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings* settings) +{ + if (settings->WantDelete) + BeginDisabled(); + Text("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", + settings->ID, settings->GetName(), settings->Pos.x, settings->Pos.y, settings->Size.x, settings->Size.y, settings->Collapsed); + if (settings->WantDelete) + EndDisabled(); +} + +void ImGui::DebugNodeWindowsList(ImVector* windows, const char* label) +{ + if (!TreeNode(label, "%s (%d)", label, windows->Size)) + return; + for (int i = windows->Size - 1; i >= 0; i--) // Iterate front to back + { + PushID((*windows)[i]); + DebugNodeWindow((*windows)[i], "Window"); + PopID(); + } + TreePop(); +} + +// FIXME-OPT: This is technically suboptimal, but it is simpler this way. +void ImGui::DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack) +{ + for (int i = 0; i < windows_size; i++) + { + ImGuiWindow* window = windows[i]; + if (window->ParentWindowInBeginStack != parent_in_begin_stack) + continue; + char buf[20]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "[%04d] Window", window->BeginOrderWithinContext); + //BulletText("[%04d] Window '%s'", window->BeginOrderWithinContext, window->Name); + DebugNodeWindow(window, buf); + Indent(); + DebugNodeWindowsListByBeginStackParent(windows + i + 1, windows_size - i - 1, window); + Unindent(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DEBUG LOG WINDOW +//----------------------------------------------------------------------------- + +void ImGui::DebugLog(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + DebugLogV(fmt, args); + va_end(args); +} + +void ImGui::DebugLogV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + const int old_size = g.DebugLogBuf.size(); + if (g.ContextName[0] != 0) + g.DebugLogBuf.appendf("[%s] [%05d] ", g.ContextName, g.FrameCount); + else + g.DebugLogBuf.appendf("[%05d] ", g.FrameCount); + g.DebugLogBuf.appendfv(fmt, args); + g.DebugLogIndex.append(g.DebugLogBuf.c_str(), old_size, g.DebugLogBuf.size()); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTTY) + IMGUI_DEBUG_PRINTF("%s", g.DebugLogBuf.begin() + old_size); +#ifdef IMGUI_ENABLE_TEST_ENGINE + // IMGUI_TEST_ENGINE_LOG() adds a trailing \n automatically + const int new_size = g.DebugLogBuf.size(); + const bool trailing_carriage_return = (g.DebugLogBuf[new_size - 1] == '\n'); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTestEngine) + IMGUI_TEST_ENGINE_LOG("%.*s", new_size - old_size - (trailing_carriage_return ? 1 : 0), g.DebugLogBuf.begin() + old_size); +#endif +} + +// FIXME-LAYOUT: To be done automatically via layout mode once we rework ItemSize/ItemAdd into ItemLayout. +static void SameLineOrWrap(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos(window->DC.CursorPosPrevLine.x + g.Style.ItemSpacing.x, window->DC.CursorPosPrevLine.y); + if (window->WorkRect.Contains(ImRect(pos, pos + size))) + ImGui::SameLine(); +} + +static void ShowDebugLogFlag(const char* name, ImGuiDebugLogFlags flags) +{ + ImGuiContext& g = *GImGui; + ImVec2 size(ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui::CalcTextSize(name).x, ImGui::GetFrameHeight()); + SameLineOrWrap(size); // FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout. + if (ImGui::CheckboxFlags(name, &g.DebugLogFlags, flags) && g.IO.KeyShift && (g.DebugLogFlags & flags) != 0) + { + g.DebugLogAutoDisableFrames = 2; + g.DebugLogAutoDisableFlags |= flags; + } + ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); +} + +void ImGui::ShowDebugLogWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 12.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui Debug Log", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + ImGuiDebugLogFlags all_enable_flags = ImGuiDebugLogFlags_EventMask_ & ~ImGuiDebugLogFlags_EventInputRouting; + CheckboxFlags("All", &g.DebugLogFlags, all_enable_flags); + SetItemTooltip("(except InputRouting which is spammy)"); + + ShowDebugLogFlag("ActiveId", ImGuiDebugLogFlags_EventActiveId); + ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper); + ShowDebugLogFlag("Focus", ImGuiDebugLogFlags_EventFocus); + ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO); + ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav); + ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup); + ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); + ShowDebugLogFlag("InputRouting", ImGuiDebugLogFlags_EventInputRouting); + + if (SmallButton("Clear")) + { + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + } + SameLine(); + if (SmallButton("Copy")) + SetClipboardText(g.DebugLogBuf.c_str()); + SameLine(); + if (SmallButton("Configure Outputs..")) + OpenPopup("Outputs"); + if (BeginPopup("Outputs")) + { + CheckboxFlags("OutputToTTY", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTTY); +#ifndef IMGUI_ENABLE_TEST_ENGINE + BeginDisabled(); +#endif + CheckboxFlags("OutputToTestEngine", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTestEngine); +#ifndef IMGUI_ENABLE_TEST_ENGINE + EndDisabled(); +#endif + EndPopup(); + } + + BeginChild("##log", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + + const ImGuiDebugLogFlags backup_log_flags = g.DebugLogFlags; + g.DebugLogFlags &= ~ImGuiDebugLogFlags_EventClipper; + + ImGuiListClipper clipper; + clipper.Begin(g.DebugLogIndex.size()); + while (clipper.Step()) + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + DebugTextUnformattedWithLocateItem(g.DebugLogIndex.get_line_begin(g.DebugLogBuf.c_str(), line_no), g.DebugLogIndex.get_line_end(g.DebugLogBuf.c_str(), line_no)); + g.DebugLogFlags = backup_log_flags; + if (GetScrollY() >= GetScrollMaxY()) + SetScrollHereY(1.0f); + EndChild(); + + End(); +} + +// Display line, search for 0xXXXXXXXX identifiers and call DebugLocateItemOnHover() when hovered. +void ImGui::DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end) +{ + TextUnformatted(line_begin, line_end); + if (!IsItemHovered()) + return; + ImGuiContext& g = *GImGui; + ImRect text_rect = g.LastItemData.Rect; + for (const char* p = line_begin; p <= line_end - 10; p++) + { + ImGuiID id = 0; + if (p[0] != '0' || (p[1] != 'x' && p[1] != 'X') || sscanf(p + 2, "%X", &id) != 1 || ImCharIsXdigitA(p[10])) + continue; + ImVec2 p0 = CalcTextSize(line_begin, p); + ImVec2 p1 = CalcTextSize(p, p + 10); + g.LastItemData.Rect = ImRect(text_rect.Min + ImVec2(p0.x, 0.0f), text_rect.Min + ImVec2(p0.x + p1.x, p1.y)); + if (IsMouseHoveringRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, true)) + DebugLocateItemOnHover(id); + p += 10; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) +//----------------------------------------------------------------------------- + +// Draw a small cross at current CursorPos in current window's DrawList +void ImGui::DebugDrawCursorPos(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos = window->DC.CursorPos; + window->DrawList->AddLine(ImVec2(pos.x, pos.y - 3.0f), ImVec2(pos.x, pos.y + 4.0f), col, 1.0f); + window->DrawList->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f); +} + +// Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList +void ImGui::DebugDrawLineExtents(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float curr_x = window->DC.CursorPos.x; + float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y); + float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(curr_x + 5.0f, line_y1), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 0.5f, line_y1), ImVec2(curr_x - 0.5f, line_y2), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y2), ImVec2(curr_x + 5.0f, line_y2), col, 1.0f); +} + +// Draw last item rect in ForegroundDrawList (so it is always visible) +void ImGui::DebugDrawItemRect(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); +} + +// [DEBUG] Locate item position/rectangle given an ID. +static const ImU32 DEBUG_LOCATE_ITEM_COLOR = IM_COL32(0, 255, 0, 255); // Green + +void ImGui::DebugLocateItem(ImGuiID target_id) +{ + ImGuiContext& g = *GImGui; + g.DebugLocateId = target_id; + g.DebugLocateFrames = 2; + g.DebugBreakInLocateId = false; +} + +// FIXME: Doesn't work over through a modal window, because they clear HoveredWindow. +void ImGui::DebugLocateItemOnHover(ImGuiID target_id) +{ + if (target_id == 0 || !IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return; + ImGuiContext& g = *GImGui; + DebugLocateItem(target_id); + GetForegroundDrawList(g.CurrentWindow)->AddRect(g.LastItemData.Rect.Min - ImVec2(3.0f, 3.0f), g.LastItemData.Rect.Max + ImVec2(3.0f, 3.0f), DEBUG_LOCATE_ITEM_COLOR); + + // Can't easily use a context menu here because it will mess with focus, active id etc. + if (g.IO.ConfigDebugIsDebuggerPresent && g.MouseStationaryTimer > 1.0f) + { + DebugBreakButtonTooltip(false, "in ItemAdd()"); + if (IsKeyChordPressed(g.DebugBreakKeyChord)) + g.DebugBreakInLocateId = true; + } +} + +void ImGui::DebugLocateItemResolveWithLastItem() +{ + ImGuiContext& g = *GImGui; + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInLocateId) + IM_DEBUG_BREAK(); + + ImGuiLastItemData item_data = g.LastItemData; + g.DebugLocateId = 0; + ImDrawList* draw_list = GetForegroundDrawList(g.CurrentWindow); + ImRect r = item_data.Rect; + r.Expand(3.0f); + ImVec2 p1 = g.IO.MousePos; + ImVec2 p2 = ImVec2((p1.x < r.Min.x) ? r.Min.x : (p1.x > r.Max.x) ? r.Max.x : p1.x, (p1.y < r.Min.y) ? r.Min.y : (p1.y > r.Max.y) ? r.Max.y : p1.y); + draw_list->AddRect(r.Min, r.Max, DEBUG_LOCATE_ITEM_COLOR); + draw_list->AddLine(p1, p2, DEBUG_LOCATE_ITEM_COLOR); +} + +void ImGui::DebugStartItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerActive = true; +} + +// [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. +void ImGui::UpdateDebugToolItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerBreakId = 0; + if (!g.DebugItemPickerActive) + return; + + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + SetMouseCursor(ImGuiMouseCursor_Hand); + if (IsKeyPressed(ImGuiKey_Escape)) + g.DebugItemPickerActive = false; + const bool change_mapping = g.IO.KeyMods == (ImGuiMod_Ctrl | ImGuiMod_Shift); + if (!change_mapping && IsMouseClicked(g.DebugItemPickerMouseButton) && hovered_id) + { + g.DebugItemPickerBreakId = hovered_id; + g.DebugItemPickerActive = false; + } + for (int mouse_button = 0; mouse_button < 3; mouse_button++) + if (change_mapping && IsMouseClicked(mouse_button)) + g.DebugItemPickerMouseButton = (ImU8)mouse_button; + SetNextWindowBgAlpha(0.70f); + if (!BeginTooltip()) + return; + Text("HoveredId: 0x%08X", hovered_id); + Text("Press ESC to abort picking."); + const char* mouse_button_names[] = { "Left", "Right", "Middle" }; + if (change_mapping) + Text("Remap w/ Ctrl+Shift: click anywhere to select new mouse button."); + else + TextColored(GetStyleColorVec4(hovered_id ? ImGuiCol_Text : ImGuiCol_TextDisabled), "Click %s Button to break in debugger! (remap w/ Ctrl+Shift)", mouse_button_names[g.DebugItemPickerMouseButton]); + EndTooltip(); +} + +// [DEBUG] ID Stack Tool: update queries. Called by NewFrame() +void ImGui::UpdateDebugToolStackQueries() +{ + ImGuiContext& g = *GImGui; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Clear hook when id stack tool is not visible + g.DebugHookIdInfo = 0; + if (g.FrameCount != tool->LastActiveFrame + 1) + return; + + // Update queries. The steps are: -1: query Stack, >= 0: query each stack item + // We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time + const ImGuiID query_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId; + if (tool->QueryId != query_id) + { + tool->QueryId = query_id; + tool->StackLevel = -1; + tool->Results.resize(0); + } + if (query_id == 0) + return; + + // Advance to next stack level when we got our result, or after 2 frames (in case we never get a result) + int stack_level = tool->StackLevel; + if (stack_level >= 0 && stack_level < tool->Results.Size) + if (tool->Results[stack_level].QuerySuccess || tool->Results[stack_level].QueryFrameCount > 2) + tool->StackLevel++; + + // Update hook + stack_level = tool->StackLevel; + if (stack_level == -1) + g.DebugHookIdInfo = query_id; + if (stack_level >= 0 && stack_level < tool->Results.Size) + { + g.DebugHookIdInfo = tool->Results[stack_level].ID; + tool->Results[stack_level].QueryFrameCount++; + } +} + +// [DEBUG] ID Stack tool: hooks called by GetID() family functions +void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Step 0: stack query + // This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget. + if (tool->StackLevel == -1) + { + tool->StackLevel++; + tool->Results.resize(window->IDStack.Size + 1, ImGuiStackLevelInfo()); + for (int n = 0; n < window->IDStack.Size + 1; n++) + tool->Results[n].ID = (n < window->IDStack.Size) ? window->IDStack[n] : id; + return; + } + + // Step 1+: query for individual level + IM_ASSERT(tool->StackLevel >= 0); + if (tool->StackLevel != window->IDStack.Size) + return; + ImGuiStackLevelInfo* info = &tool->Results[tool->StackLevel]; + IM_ASSERT(info->ID == id && info->QueryFrameCount > 0); + + switch (data_type) + { + case ImGuiDataType_S32: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%d", (int)(intptr_t)data_id); + break; + case ImGuiDataType_String: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%.*s", data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)strlen((const char*)data_id), (const char*)data_id); + break; + case ImGuiDataType_Pointer: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "(void*)0x%p", data_id); + break; + case ImGuiDataType_ID: + if (info->Desc[0] != 0) // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one. + return; + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "0x%08X [override]", id); + break; + default: + IM_ASSERT(0); + } + info->QuerySuccess = true; + info->DataType = data_type; +} + +static int StackToolFormatLevelInfo(ImGuiIDStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size) +{ + ImGuiStackLevelInfo* info = &tool->Results[n]; + ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL; + if (window) // Source: window name (because the root ID don't call GetID() and so doesn't get hooked) + return ImFormatString(buf, buf_size, format_for_ui ? "\"%s\" [window]" : "%s", window->Name); + if (info->QuerySuccess) // Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id) + return ImFormatString(buf, buf_size, (format_for_ui && info->DataType == ImGuiDataType_String) ? "\"%s\"" : "%s", info->Desc); + if (tool->StackLevel < tool->Results.Size) // Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers. + return (*buf = 0); +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (const char* label = ImGuiTestEngine_FindItemDebugLabel(GImGui, info->ID)) // Source: ImGuiTestEngine's ItemInfo() + return ImFormatString(buf, buf_size, format_for_ui ? "??? \"%s\"" : "%s", label); +#endif + return ImFormatString(buf, buf_size, "???"); +} + +// ID Stack Tool: Display UI +void ImGui::ShowIDStackToolWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 8.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui ID Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // Display hovered/active status + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + const ImGuiID active_id = g.ActiveId; +#ifdef IMGUI_ENABLE_TEST_ENGINE + Text("HoveredId: 0x%08X (\"%s\"), ActiveId: 0x%08X (\"%s\")", hovered_id, hovered_id ? ImGuiTestEngine_FindItemDebugLabel(&g, hovered_id) : "", active_id, active_id ? ImGuiTestEngine_FindItemDebugLabel(&g, active_id) : ""); +#else + Text("HoveredId: 0x%08X, ActiveId: 0x%08X", hovered_id, active_id); +#endif + SameLine(); + MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details."); + + // CTRL+C to copy path + const float time_since_copy = (float)g.Time - tool->CopyToClipboardLastTime; + Checkbox("Ctrl+C: copy path to clipboard", &tool->CopyToClipboardOnCtrlC); + SameLine(); + TextColored((time_since_copy >= 0.0f && time_since_copy < 0.75f && ImFmod(time_since_copy, 0.25f) < 0.25f * 0.5f) ? ImVec4(1.f, 1.f, 0.3f, 1.f) : ImVec4(), "*COPIED*"); + if (tool->CopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused)) + { + tool->CopyToClipboardLastTime = (float)g.Time; + char* p = g.TempBuffer.Data; + char* p_end = p + g.TempBuffer.Size; + for (int stack_n = 0; stack_n < tool->Results.Size && p + 3 < p_end; stack_n++) + { + *p++ = '/'; + char level_desc[256]; + StackToolFormatLevelInfo(tool, stack_n, false, level_desc, IM_ARRAYSIZE(level_desc)); + for (int n = 0; level_desc[n] && p + 2 < p_end; n++) + { + if (level_desc[n] == '/') + *p++ = '\\'; + *p++ = level_desc[n]; + } + } + *p = '\0'; + SetClipboardText(g.TempBuffer.Data); + } + + // Display decorated stack + tool->LastActiveFrame = g.FrameCount; + if (tool->Results.Size > 0 && BeginTable("##table", 3, ImGuiTableFlags_Borders)) + { + const float id_width = CalcTextSize("0xDDDDDDDD").x; + TableSetupColumn("Seed", ImGuiTableColumnFlags_WidthFixed, id_width); + TableSetupColumn("PushID", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Result", ImGuiTableColumnFlags_WidthFixed, id_width); + TableHeadersRow(); + for (int n = 0; n < tool->Results.Size; n++) + { + ImGuiStackLevelInfo* info = &tool->Results[n]; + TableNextColumn(); + Text("0x%08X", (n > 0) ? tool->Results[n - 1].ID : 0); + TableNextColumn(); + StackToolFormatLevelInfo(tool, n, true, g.TempBuffer.Data, g.TempBuffer.Size); + TextUnformatted(g.TempBuffer.Data); + TableNextColumn(); + Text("0x%08X", info->ID); + if (n == tool->Results.Size - 1) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_Header)); + } + EndTable(); + } + End(); +} + +#else + +void ImGui::ShowMetricsWindow(bool*) {} +void ImGui::ShowFontAtlas(ImFontAtlas*) {} +void ImGui::DebugNodeColumns(ImGuiOldColumns*) {} +void ImGui::DebugNodeDrawList(ImGuiWindow*, ImGuiViewportP*, const ImDrawList*, const char*) {} +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {} +void ImGui::DebugNodeFont(ImFont*) {} +void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {} +void ImGui::DebugNodeTabBar(ImGuiTabBar*, const char*) {} +void ImGui::DebugNodeWindow(ImGuiWindow*, const char*) {} +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings*) {} +void ImGui::DebugNodeWindowsList(ImVector*, const char*) {} +void ImGui::DebugNodeViewport(ImGuiViewportP*) {} + +void ImGui::ShowDebugLogWindow(bool*) {} +void ImGui::ShowIDStackToolWindow(bool*) {} +void ImGui::DebugStartItemPicker() {} +void ImGui::DebugHookIdInfo(ImGuiID, ImGuiDataType, const void*, const void*) {} + +#endif // #ifndef IMGUI_DISABLE_DEBUG_TOOLS + +//----------------------------------------------------------------------------- + +// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. +// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. +#ifdef IMGUI_INCLUDE_IMGUI_USER_INL +#include "imgui_user.inl" +#endif + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/attachments/simple_engine/imgui/imgui.h b/attachments/simple_engine/imgui/imgui.h index 4b45b0f97..a6766e2d6 100644 --- a/attachments/simple_engine/imgui/imgui.h +++ b/attachments/simple_engine/imgui/imgui.h @@ -1,2276 +1,3676 @@ -// dear imgui, v1.60 WIP +// dear imgui, v1.91.1 // (headers) -// See imgui.cpp file for documentation. -// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. -// Read 'Programmer guide' in imgui.cpp for notes on how to setup ImGui in your codebase. -// Get latest version at https://github.com/ocornut/imgui +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. + +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues/7503 (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Library Version +// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') +#define IMGUI_VERSION "1.91.1" +#define IMGUI_VERSION_NUM 19110 +#define IMGUI_HAS_TABLE + +/* + +Index of this file: +// [SECTION] Header mess +// [SECTION] Forward declarations and basic types +// [SECTION] Dear ImGui end-user API functions +// [SECTION] Flags & Enumerations +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Helpers: Debug log, Memory allocations macros, ImVector<> +// [SECTION] ImGuiStyle +// [SECTION] ImGuiIO +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage, ImGuiSelectionExternalStorage) +// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) +// [SECTION] ImGuiPlatformIO + other Platform Dependent Interfaces (ImGuiPlatformImeData) +// [SECTION] Obsolete functions and types + +*/ #pragma once -// User-editable configuration files (edit stock imconfig.h or define IMGUI_USER_CONFIG to your own filename) +// Configuration file with compile-time options +// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system) #ifdef IMGUI_USER_CONFIG -# include IMGUI_USER_CONFIG -#endif -#if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_INCLUDE_IMCONFIG_H) -# include "imconfig.h" +#include IMGUI_USER_CONFIG #endif +#include "imconfig.h" -#include // FLT_MAX -#include // va_list -#include // ptrdiff_t, NULL -#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp +#ifndef IMGUI_DISABLE -#define IMGUI_VERSION "1.60 WIP" +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- -// Define attributes of all API symbols declarations, e.g. for DLL under Windows. +// Includes +#include // FLT_MIN, FLT_MAX +#include // va_list, va_start, va_end +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp + +// Define attributes of all API symbols declarations (e.g. for DLL under Windows) +// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) +// Using dear imgui via a shared library is not recommended: we don't guarantee backward nor forward ABI compatibility + this is a call-heavy library and function call overhead adds up. #ifndef IMGUI_API -# define IMGUI_API +#define IMGUI_API +#endif +#ifndef IMGUI_IMPL_API +#define IMGUI_IMPL_API IMGUI_API #endif -// Define assertion handler. +// Helper Macros #ifndef IM_ASSERT -# include -# define IM_ASSERT(_EXPR) assert(_EXPR) +#include +#define IM_ASSERT(_EXPR) assert(_EXPR) // You can override the default assert handler by editing imconfig.h +#endif +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! +#define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. + +// Check that version and structures layouts are matching between compiled imgui code and caller. Read comments above DebugCheckVersionAndDataLayout() for details. +#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) + +// Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +// (MSVC provides an equivalent mechanism via SAL Annotations but it would require the macros in a different +// location. e.g. #include + void myprintf(_Printf_format_string_ const char* format, ...)) +#if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) +#define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) +#elif !defined(IMGUI_USE_STB_SPRINTF) && (defined(__clang__) || defined(__GNUC__)) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +#else +#define IM_FMTARGS(FMT) +#define IM_FMTLIST(FMT) #endif -// Helpers -// Some compilers support applying printf-style warnings to user functions. -#if defined(__clang__) || defined(__GNUC__) -# define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT + 1))) -# define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +// Disable some of MSVC most aggressive Debug runtime checks in function header/footer (used in some simple/low-level functions) +#if defined(_MSC_VER) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(IMGUI_DEBUG_PARANOID) +#define IM_MSVC_RUNTIME_CHECKS_OFF __pragma(runtime_checks("",off)) __pragma(check_stack(off)) __pragma(strict_gs_check(push,off)) +#define IM_MSVC_RUNTIME_CHECKS_RESTORE __pragma(runtime_checks("",restore)) __pragma(check_stack()) __pragma(strict_gs_check(pop)) #else -# define IM_FMTARGS(FMT) -# define IM_FMTLIST(FMT) +#define IM_MSVC_RUNTIME_CHECKS_OFF +#define IM_MSVC_RUNTIME_CHECKS_RESTORE #endif -#define IM_ARRAYSIZE(_ARR) ((int) (sizeof(_ARR) / sizeof(*_ARR))) -#define IM_OFFSETOF(_TYPE, _MEMBER) ((size_t) & (((_TYPE *) 0)->_MEMBER)) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in modern C++. +// Warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#endif #if defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic push +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations and basic types +//----------------------------------------------------------------------------- + +// Scalar data types +typedef unsigned int ImGuiID;// A unique ID used by widgets (typically the result of hashing a stack of string) +typedef signed char ImS8; // 8-bit signed integer +typedef unsigned char ImU8; // 8-bit unsigned integer +typedef signed short ImS16; // 16-bit signed integer +typedef unsigned short ImU16; // 16-bit unsigned integer +typedef signed int ImS32; // 32-bit signed integer == int +typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) +typedef signed long long ImS64; // 64-bit signed integer +typedef unsigned long long ImU64; // 64-bit unsigned integer // Forward declarations -struct ImDrawChannel; // Temporary storage for outputting drawing commands out of order, used by ImDrawList::ChannelsSplit() -struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call) -struct ImDrawData; // All draw command lists required to render the frame -struct ImDrawList; // A single draw command list (generally one per window) -struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) -struct ImDrawVert; // A single vertex (20 bytes by default, override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) -struct ImFont; // Runtime data for a single font within a parent ImFontAtlas -struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader -struct ImFontConfig; // Configuration data when adding a font or merging fonts -struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 -struct ImGuiIO; // Main configuration and I/O between your application and ImGui -struct ImGuiOnceUponAFrame; // Simple helper for running a block of code not more than once a frame, used by IMGUI_ONCE_UPON_A_FRAME macro -struct ImGuiStorage; // Simple custom key value storage -struct ImGuiStyle; // Runtime data for styling/colors -struct ImGuiTextFilter; // Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" -struct ImGuiTextBuffer; // Text buffer for logging/accumulating text -struct ImGuiTextEditCallbackData; // Shared state of ImGui::InputText() when using custom ImGuiTextEditCallback (rare/advanced use) -struct ImGuiSizeCallbackData; // Structure used to constraint window size in custom ways when using custom ImGuiSizeCallback (rare/advanced use) -struct ImGuiListClipper; // Helper to manually clip large list of items -struct ImGuiPayload; // User data payload for drag and drop operations -struct ImGuiContext; // ImGui context (opaque) - -// Typedefs and Enumerations (declared as int for compatibility and to not pollute the top of this file) -typedef unsigned int ImU32; // 32-bit unsigned integer (typically used to store packed colors) -typedef unsigned int ImGuiID; // unique ID used by widgets (typically hashed from a stack of string) -typedef unsigned short ImWchar; // character for keyboard input/display -typedef void *ImTextureID; // user data to identify a texture (this is whatever to you want it to be! read the FAQ about ImTextureID in imgui.cpp) -typedef int ImGuiCol; // enum: a color identifier for styling // enum ImGuiCol_ -typedef int ImGuiCond; // enum: a condition for Set*() // enum ImGuiCond_ -typedef int ImGuiKey; // enum: a key identifier (ImGui-side enum) // enum ImGuiKey_ -typedef int ImGuiNavInput; // enum: an input identifier for navigation // enum ImGuiNavInput_ -typedef int ImGuiMouseCursor; // enum: a mouse cursor identifier // enum ImGuiMouseCursor_ -typedef int ImGuiStyleVar; // enum: a variable identifier for styling // enum ImGuiStyleVar_ -typedef int ImDrawCornerFlags; // flags: for ImDrawList::AddRect*() etc. // enum ImDrawCornerFlags_ -typedef int ImDrawListFlags; // flags: for ImDrawList // enum ImDrawListFlags_ -typedef int ImFontAtlasFlags; // flags: for ImFontAtlas // enum ImFontAtlasFlags_ -typedef int ImGuiColorEditFlags; // flags: for ColorEdit*(), ColorPicker*() // enum ImGuiColorEditFlags_ -typedef int ImGuiColumnsFlags; // flags: for *Columns*() // enum ImGuiColumnsFlags_ -typedef int ImGuiDragDropFlags; // flags: for *DragDrop*() // enum ImGuiDragDropFlags_ -typedef int ImGuiComboFlags; // flags: for BeginCombo() // enum ImGuiComboFlags_ -typedef int ImGuiFocusedFlags; // flags: for IsWindowFocused() // enum ImGuiFocusedFlags_ -typedef int ImGuiHoveredFlags; // flags: for IsItemHovered() etc. // enum ImGuiHoveredFlags_ -typedef int ImGuiInputTextFlags; // flags: for InputText*() // enum ImGuiInputTextFlags_ -typedef int ImGuiNavFlags; // flags: for io.NavFlags // enum ImGuiNavFlags_ -typedef int ImGuiSelectableFlags; // flags: for Selectable() // enum ImGuiSelectableFlags_ -typedef int ImGuiTreeNodeFlags; // flags: for TreeNode*(),CollapsingHeader()// enum ImGuiTreeNodeFlags_ -typedef int ImGuiWindowFlags; // flags: for Begin*() // enum ImGuiWindowFlags_ -typedef int (*ImGuiTextEditCallback)(ImGuiTextEditCallbackData *data); -typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData *data); -#if defined(_MSC_VER) && !defined(__clang__) -typedef unsigned __int64 ImU64; // 64-bit unsigned integer -#else -typedef unsigned long long ImU64; // 64-bit unsigned integer +struct ImDrawChannel; // Temporary storage to output draw commands out of order, used by ImDrawListSplitter and ImDrawList::ChannelsSplit() +struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call, unless it is a callback) +struct ImDrawData; // All draw command lists required to render the frame + pos/size coordinates to use for the projection matrix. +struct ImDrawList; // A single draw command list (generally one per window, conceptually you may see this as a dynamic "mesh" builder) +struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) +struct ImDrawListSplitter; // Helper to split a draw list into different layers which can be drawn into out of order, then flattened back. +struct ImDrawVert; // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) +struct ImFont; // Runtime data for a single font within a parent ImFontAtlas +struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontBuilderIO; // Opaque interface to a font builder (stb_truetype or FreeType). +struct ImFontConfig; // Configuration data when adding a font or merging fonts +struct ImFontGlyph; // A single font glyph (code point + coordinates within in ImFontAtlas + offset) +struct ImFontGlyphRangesBuilder; // Helper to build glyph ranges from text/string data +struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using) +struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) +struct ImGuiIO; // Main configuration and I/O between your application and ImGui (also see: ImGuiPlatformIO) +struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) +struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSelect()/EndMultiSelect() block +struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformIO; // Interface between platform/renderer backends and ImGui (e.g. Clipboard, IME hooks). Extends ImGuiIO. In docking branch, this gets extended to support multi-viewports. +struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. +struct ImGuiSelectionBasicStorage; // Optional helper to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionExternalStorage;//Optional helper to apply multi-selection requests to existing randomly accessible storage. +struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) +struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) +struct ImGuiStorage; // Helper for key->value storage (container sorted by key) +struct ImGuiStoragePair; // Helper for key->value storage (pair) +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table +struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) +struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") +struct ImGuiViewport; // A Platform Window (always only one in 'master' branch), in the future may represent Platform Monitor + +// Enumerations +// - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +enum ImGuiDir : int; // -> enum ImGuiDir // Enum: A cardinal direction (Left, Right, Up, Down) +enum ImGuiKey : int; // -> enum ImGuiKey // Enum: A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value) +enum ImGuiMouseSource : int; // -> enum ImGuiMouseSource // Enum; A mouse input source identifier (Mouse, TouchScreen, Pen) +enum ImGuiSortDirection : ImU8; // -> enum ImGuiSortDirection // Enum: A sorting direction (ascending or descending) +typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling +typedef int ImGuiCond; // -> enum ImGuiCond_ // Enum: A condition for many Set*() functions +typedef int ImGuiDataType; // -> enum ImGuiDataType_ // Enum: A primary data type +typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) +typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor shape +typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling +typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() + +// Flags (declared as int to allow using as flags without overhead, and to not pollute the top of this file) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions +typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build +typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags +typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiChildFlags; // -> enum ImGuiChildFlags_ // Flags: for BeginChild() +typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. +typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags +typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() +typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() +typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. +typedef int ImGuiInputFlags; // -> enum ImGuiInputFlags_ // Flags: for Shortcut(), SetNextItemShortcut() +typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() +typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag(), shared by all items +typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. +typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() +typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() +typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() +typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() +typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() +typedef int ImGuiViewportFlags; // -> enum ImGuiViewportFlags_ // Flags: for ImGuiViewport +typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() + +// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type] +// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file. +// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details. +#ifndef ImTextureID +typedef void* ImTextureID; // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that) +#endif + +// ImDrawIdx: vertex index. [Compile-time configurable type] +// - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). +// - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibility with renderer backends) #endif -// Others helpers at bottom of the file: -// class ImVector<> // Lightweight std::vector like class. -// IMGUI_ONCE_UPON_A_FRAME // Execute a block of code once per frame only (convenient for creating UI within deep-nested code that runs multiple times) +// Character types +// (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) +typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. +#ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] +typedef ImWchar32 ImWchar; +#else +typedef ImWchar16 ImWchar; +#endif +// Multi-Selection item index or identifier when using BeginMultiSelect() +// - Used by SetNextItemSelectionUserData() + and inside ImGuiMultiSelectIO structure. +// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER/ID as well. Read comments near ImGuiMultiSelectIO for details. +typedef ImS64 ImGuiSelectionUserData; + +// Callback and functions types +typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() +typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() +typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() + +// ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] +// - This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +IM_MSVC_RUNTIME_CHECKS_OFF struct ImVec2 { - float x, y; - ImVec2() - { - x = y = 0.0f; - } - ImVec2(float _x, float _y) - { - x = _x; - y = _y; - } - float operator[](size_t idx) const - { - IM_ASSERT(idx == 0 || idx == 1); - return (&x)[idx]; - } // We very rarely use this [] operator, thus an assert is fine. -#ifdef IM_VEC2_CLASS_EXTRA // Define constructor and implicit cast operators in imconfig.h to convert back<>forth from your math types and ImVec2. - IM_VEC2_CLASS_EXTRA + float x, y; + constexpr ImVec2() : x(0.0f), y(0.0f) { } + constexpr ImVec2(float _x, float _y) : x(_x), y(_y) { } + float& operator[] (size_t idx) { IM_ASSERT(idx == 0 || idx == 1); return ((float*)(void*)(char*)this)[idx]; } // We very rarely use this [] operator, so the assert overhead is fine. + float operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return ((const float*)(const void*)(const char*)this)[idx]; } +#ifdef IM_VEC2_CLASS_EXTRA + IM_VEC2_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec2. #endif }; +// ImVec4: 4D vector used to store clipping rectangles, colors etc. [Compile-time configurable type] struct ImVec4 { - float x, y, z, w; - ImVec4() - { - x = y = z = w = 0.0f; - } - ImVec4(float _x, float _y, float _z, float _w) - { - x = _x; - y = _y; - z = _z; - w = _w; - } -#ifdef IM_VEC4_CLASS_EXTRA // Define constructor and implicit cast operators in imconfig.h to convert back<>forth from your math types and ImVec4. - IM_VEC4_CLASS_EXTRA + float x, y, z, w; + constexpr ImVec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) { } + constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) { } +#ifdef IM_VEC4_CLASS_EXTRA + IM_VEC4_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec4. #endif }; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] Dear ImGui end-user API functions +// (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!) +//----------------------------------------------------------------------------- -// ImGui end-user API -// In a namespace so that user can add extra functions in a separate file (e.g. Value() helpers for your vector or common types) namespace ImGui { -// Context creation and access, if you want to use multiple context, share context between modules (e.g. DLL). -// All contexts share a same ImFontAtlas by default. If you want different font atlas, you can new() them and overwrite the GetIO().Fonts variable of an ImGui context. -// All those functions are not reliant on the current context. -IMGUI_API ImGuiContext *CreateContext(ImFontAtlas *shared_font_atlas = NULL); -IMGUI_API void DestroyContext(ImGuiContext *ctx = NULL); // NULL = Destroy current context -IMGUI_API ImGuiContext *GetCurrentContext(); -IMGUI_API void SetCurrentContext(ImGuiContext *ctx); - -// Main -IMGUI_API ImGuiIO &GetIO(); -IMGUI_API ImGuiStyle &GetStyle(); -IMGUI_API void NewFrame(); // start a new ImGui frame, you can submit any command from this point until Render()/EndFrame(). -IMGUI_API void Render(); // ends the ImGui frame, finalize the draw data. (Obsolete: optionally call io.RenderDrawListsFn if set. Nowadays, prefer calling your render function yourself.) -IMGUI_API ImDrawData *GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. (Obsolete: this used to be passed to your io.RenderDrawListsFn() function.) -IMGUI_API void EndFrame(); // ends the ImGui frame. automatically called by Render(), so most likely don't need to ever call that yourself directly. If you don't need to render you may call EndFrame() but you'll have wasted CPU already. If you don't need to render, better to not create any imgui windows instead! - -// Demo, Debug, Informations -IMGUI_API void ShowDemoWindow(bool *p_open = NULL); // create demo/test window (previously called ShowTestWindow). demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! -IMGUI_API void ShowMetricsWindow(bool *p_open = NULL); // create metrics window. display ImGui internals: draw commands (with individual draw calls and vertices), window list, basic internal state, etc. -IMGUI_API void ShowStyleEditor(ImGuiStyle *ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) -IMGUI_API bool ShowStyleSelector(const char *label); -IMGUI_API void ShowFontSelector(const char *label); -IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as a end-user (mouse/keyboard controls). -IMGUI_API const char *GetVersion(); - -// Styles -IMGUI_API void StyleColorsDark(ImGuiStyle *dst = NULL); // New, recommended style -IMGUI_API void StyleColorsClassic(ImGuiStyle *dst = NULL); // Classic imgui style (default) -IMGUI_API void StyleColorsLight(ImGuiStyle *dst = NULL); // Best used with borders and a custom, thicker font - -// Window -IMGUI_API bool Begin(const char *name, bool *p_open = NULL, ImGuiWindowFlags flags = 0); // push window to the stack and start appending to it. see .cpp for details. return false when window is collapsed (so you can early out in your code) but you always need to call End() regardless. 'bool* p_open' creates a widget on the upper-right to close the window (which sets your bool to false). -IMGUI_API void End(); // always call even if Begin() return false (which indicates a collapsed window)! finish appending to current window, pop it off the window stack. -IMGUI_API bool BeginChild(const char *str_id, const ImVec2 &size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); // begin a scrolling region. size==0.0f: use remaining window size, size<0.0f: use remaining window size minus abs(size). size>0.0f: fixed size. each axis can use a different mode, e.g. ImVec2(0,400). -IMGUI_API bool BeginChild(ImGuiID id, const ImVec2 &size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); // " -IMGUI_API void EndChild(); // always call even if BeginChild() return false (which indicates a collapsed or clipping child window) -IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates -IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() -IMGUI_API float GetContentRegionAvailWidth(); // -IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min (roughly (0,0)-Scroll), in window coordinates -IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max (roughly (0,0)+Size-Scroll) where Size can be override with SetNextWindowContentSize(), in window coordinates -IMGUI_API float GetWindowContentRegionWidth(); // -IMGUI_API ImDrawList *GetWindowDrawList(); // get rendering command-list if you want to append your own draw primitives -IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList api) -IMGUI_API ImVec2 GetWindowSize(); // get current window size -IMGUI_API float GetWindowWidth(); -IMGUI_API float GetWindowHeight(); -IMGUI_API bool IsWindowCollapsed(); -IMGUI_API bool IsWindowAppearing(); -IMGUI_API void SetWindowFontScale(float scale); // per-window font scale. Adjust IO.FontGlobalScale if you want to scale all windows - -IMGUI_API void SetNextWindowPos(const ImVec2 &pos, ImGuiCond cond = 0, const ImVec2 &pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. -IMGUI_API void SetNextWindowSize(const ImVec2 &size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() -IMGUI_API void SetNextWindowSizeConstraints(const ImVec2 &size_min, const ImVec2 &size_max, ImGuiSizeCallback custom_callback = NULL, void *custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Use callback to apply non-trivial programmatic constraints. -IMGUI_API void SetNextWindowContentSize(const ImVec2 &size); // set next window content size (~ enforce the range of scrollbars). not including window decorations (title bar, menu bar, etc.). set an axis to 0.0f to leave it automatic. call before Begin() -IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() -IMGUI_API void SetNextWindowFocus(); // set next window to be focused / front-most. call before Begin() -IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily modify ImGuiCol_WindowBg/ChildBg/PopupBg. -IMGUI_API void SetWindowPos(const ImVec2 &pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. -IMGUI_API void SetWindowSize(const ImVec2 &size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0,0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. -IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). -IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / front-most. prefer using SetNextWindowFocus(). -IMGUI_API void SetWindowPos(const char *name, const ImVec2 &pos, ImGuiCond cond = 0); // set named window position. -IMGUI_API void SetWindowSize(const char *name, const ImVec2 &size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. -IMGUI_API void SetWindowCollapsed(const char *name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state -IMGUI_API void SetWindowFocus(const char *name); // set named window to be focused / front-most. use NULL to remove focus. - -IMGUI_API float GetScrollX(); // get scrolling amount [0..GetScrollMaxX()] -IMGUI_API float GetScrollY(); // get scrolling amount [0..GetScrollMaxY()] -IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.X - WindowSize.X -IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.Y - WindowSize.Y -IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0..GetScrollMaxX()] -IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0..GetScrollMaxY()] -IMGUI_API void SetScrollHere(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. -IMGUI_API void SetScrollFromPosY(float pos_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position valid. use GetCursorPos() or GetCursorStartPos()+offset to get valid positions. -IMGUI_API void SetStateStorage(ImGuiStorage *tree); // replace tree state storage with our own (if you want to manipulate it yourself, typically clear subsection of it) -IMGUI_API ImGuiStorage *GetStateStorage(); - -// Parameters stacks (shared) -IMGUI_API void PushFont(ImFont *font); // use NULL as a shortcut to push default font -IMGUI_API void PopFont(); -IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); -IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4 &col); -IMGUI_API void PopStyleColor(int count = 1); -IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); -IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2 &val); -IMGUI_API void PopStyleVar(int count = 1); -IMGUI_API const ImVec4 &GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwhise use GetColorU32() to get style color + style alpha. -IMGUI_API ImFont *GetFont(); // get current font -IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied -IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API -IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier -IMGUI_API ImU32 GetColorU32(const ImVec4 &col); // retrieve given color with style alpha applied -IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied - -// Parameters stacks (current window) -IMGUI_API void PushItemWidth(float item_width); // width of items for the common item+label case, pixels. 0.0f = default to ~2/3 of windows width, >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -1.0f always align width to the right side) -IMGUI_API void PopItemWidth(); -IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position -IMGUI_API void PushTextWrapPos(float wrap_pos_x = 0.0f); // word-wrapping for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space -IMGUI_API void PopTextWrapPos(); -IMGUI_API void PushAllowKeyboardFocus(bool allow_keyboard_focus); // allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets -IMGUI_API void PopAllowKeyboardFocus(); -IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. -IMGUI_API void PopButtonRepeat(); - -// Cursor / Layout -IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. -IMGUI_API void SameLine(float pos_x = 0.0f, float spacing_w = -1.0f); // call between widgets or groups to layout them horizontally -IMGUI_API void NewLine(); // undo a SameLine() -IMGUI_API void Spacing(); // add vertical spacing -IMGUI_API void Dummy(const ImVec2 &size); // add a dummy item of given size -IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by style.IndentSpacing or indent_w if != 0 -IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by style.IndentSpacing or indent_w if != 0 -IMGUI_API void BeginGroup(); // lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) -IMGUI_API void EndGroup(); -IMGUI_API ImVec2 GetCursorPos(); // cursor position is relative to window position -IMGUI_API float GetCursorPosX(); // " -IMGUI_API float GetCursorPosY(); // " -IMGUI_API void SetCursorPos(const ImVec2 &local_pos); // " -IMGUI_API void SetCursorPosX(float x); // " -IMGUI_API void SetCursorPosY(float y); // " -IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position -IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute screen coordinates [0..io.DisplaySize] (useful to work with ImDrawList API) -IMGUI_API void SetCursorScreenPos(const ImVec2 &pos); // cursor position in absolute screen coordinates [0..io.DisplaySize] -IMGUI_API void AlignTextToFramePadding(); // vertically align/lower upcoming text to FramePadding.y so that it will aligns to upcoming widgets (call if you have text on a line before regular widgets) -IMGUI_API float GetTextLineHeight(); // ~ FontSize -IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) -IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 -IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) - -// Columns -// You can also use SameLine(pos_x) for simplified columns. The columns API is still work-in-progress and rather lacking. -IMGUI_API void Columns(int count = 1, const char *id = NULL, bool border = true); -IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished -IMGUI_API int GetColumnIndex(); // get current column index -IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column -IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column -IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f -IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column -IMGUI_API int GetColumnsCount(); - -// ID scopes -// If you are creating widgets in a loop you most likely want to push a unique identifier (e.g. object pointer, loop index) so ImGui can differentiate them. -// You can also use the "##foobar" syntax within widget label to distinguish them from each others. Read "A primer on the use of labels/IDs" in the FAQ for more details. -IMGUI_API void PushID(const char *str_id); // push identifier into the ID stack. IDs are hash of the entire stack! -IMGUI_API void PushID(const char *str_id_begin, const char *str_id_end); -IMGUI_API void PushID(const void *ptr_id); -IMGUI_API void PushID(int int_id); -IMGUI_API void PopID(); -IMGUI_API ImGuiID GetID(const char *str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself -IMGUI_API ImGuiID GetID(const char *str_id_begin, const char *str_id_end); -IMGUI_API ImGuiID GetID(const void *ptr_id); - -// Widgets: Text -IMGUI_API void TextUnformatted(const char *text, const char *text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. -IMGUI_API void Text(const char *fmt, ...) IM_FMTARGS(1); // simple formatted text -IMGUI_API void TextV(const char *fmt, va_list args) IM_FMTLIST(1); -IMGUI_API void TextColored(const ImVec4 &col, const char *fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); -IMGUI_API void TextColoredV(const ImVec4 &col, const char *fmt, va_list args) IM_FMTLIST(2); -IMGUI_API void TextDisabled(const char *fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); -IMGUI_API void TextDisabledV(const char *fmt, va_list args) IM_FMTLIST(1); -IMGUI_API void TextWrapped(const char *fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). -IMGUI_API void TextWrappedV(const char *fmt, va_list args) IM_FMTLIST(1); -IMGUI_API void LabelText(const char *label, const char *fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets -IMGUI_API void LabelTextV(const char *label, const char *fmt, va_list args) IM_FMTLIST(2); -IMGUI_API void BulletText(const char *fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() -IMGUI_API void BulletTextV(const char *fmt, va_list args) IM_FMTLIST(1); -IMGUI_API void Bullet(); // draw a small circle and keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses - -// Widgets: Main -IMGUI_API bool Button(const char *label, const ImVec2 &size = ImVec2(0, 0)); // button -IMGUI_API bool SmallButton(const char *label); // button with FramePadding=(0,0) to easily embed within text -IMGUI_API bool InvisibleButton(const char *str_id, const ImVec2 &size); // button behavior without the visuals, useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) -IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2 &size, const ImVec2 &uv0 = ImVec2(0, 0), const ImVec2 &uv1 = ImVec2(1, 1), const ImVec4 &tint_col = ImVec4(1, 1, 1, 1), const ImVec4 &border_col = ImVec4(0, 0, 0, 0)); -IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2 &size, const ImVec2 &uv0 = ImVec2(0, 0), const ImVec2 &uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4 &bg_col = ImVec4(0, 0, 0, 0), const ImVec4 &tint_col = ImVec4(1, 1, 1, 1)); // <0 frame_padding uses default frame padding settings. 0 for no padding -IMGUI_API bool Checkbox(const char *label, bool *v); -IMGUI_API bool CheckboxFlags(const char *label, unsigned int *flags, unsigned int flags_value); -IMGUI_API bool RadioButton(const char *label, bool active); -IMGUI_API bool RadioButton(const char *label, int *v, int v_button); -IMGUI_API void PlotLines(const char *label, const float *values, int values_count, int values_offset = 0, const char *overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); -IMGUI_API void PlotLines(const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset = 0, const char *overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); -IMGUI_API void PlotHistogram(const char *label, const float *values, int values_count, int values_offset = 0, const char *overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); -IMGUI_API void PlotHistogram(const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset = 0, const char *overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); -IMGUI_API void ProgressBar(float fraction, const ImVec2 &size_arg = ImVec2(-1, 0), const char *overlay = NULL); - -// Widgets: Combo Box -// The new BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it. -// The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. -IMGUI_API bool BeginCombo(const char *label, const char *preview_value, ImGuiComboFlags flags = 0); -IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! -IMGUI_API bool Combo(const char *label, int *current_item, const char *const items[], int items_count, int popup_max_height_in_items = -1); -IMGUI_API bool Combo(const char *label, int *current_item, const char *items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" -IMGUI_API bool Combo(const char *label, int *current_item, bool (*items_getter)(void *data, int idx, const char **out_text), void *data, int items_count, int popup_max_height_in_items = -1); - -// Widgets: Drags (tip: ctrl+click on a drag box to input with keyboard. manually input values aren't clamped, can go off-bounds) -// For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every functions, note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x -// Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). -IMGUI_API bool DragFloat(const char *label, float *v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *display_format = "%.3f", float power = 1.0f); // If v_min >= v_max we have no bound -IMGUI_API bool DragFloat2(const char *label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool DragFloat3(const char *label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool DragFloat4(const char *label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool DragFloatRange2(const char *label, float *v_current_min, float *v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *display_format = "%.3f", const char *display_format_max = NULL, float power = 1.0f); -IMGUI_API bool DragInt(const char *label, int *v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *display_format = "%.0f"); // If v_min >= v_max we have no bound -IMGUI_API bool DragInt2(const char *label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *display_format = "%.0f"); -IMGUI_API bool DragInt3(const char *label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *display_format = "%.0f"); -IMGUI_API bool DragInt4(const char *label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *display_format = "%.0f"); -IMGUI_API bool DragIntRange2(const char *label, int *v_current_min, int *v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *display_format = "%.0f", const char *display_format_max = NULL); - -// Widgets: Input with Keyboard -IMGUI_API bool InputText(const char *label, char *buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void *user_data = NULL); -IMGUI_API bool InputTextMultiline(const char *label, char *buf, size_t buf_size, const ImVec2 &size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void *user_data = NULL); -IMGUI_API bool InputFloat(const char *label, float *v, float step = 0.0f, float step_fast = 0.0f, int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputFloat2(const char *label, float v[2], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputFloat3(const char *label, float v[3], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputFloat4(const char *label, float v[4], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputInt(const char *label, int *v, int step = 1, int step_fast = 100, ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputInt2(const char *label, int v[2], ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputInt3(const char *label, int v[3], ImGuiInputTextFlags extra_flags = 0); -IMGUI_API bool InputInt4(const char *label, int v[4], ImGuiInputTextFlags extra_flags = 0); - -// Widgets: Sliders (tip: ctrl+click on a slider to input with keyboard. manually input values aren't clamped, can go off-bounds) -IMGUI_API bool SliderFloat(const char *label, float *v, float v_min, float v_max, const char *display_format = "%.3f", float power = 1.0f); // adjust display_format to decorate the value with a prefix or a suffix for in-slider labels or unit display. Use power!=1.0 for logarithmic sliders -IMGUI_API bool SliderFloat2(const char *label, float v[2], float v_min, float v_max, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool SliderFloat3(const char *label, float v[3], float v_min, float v_max, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool SliderFloat4(const char *label, float v[4], float v_min, float v_max, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool SliderAngle(const char *label, float *v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f); -IMGUI_API bool SliderInt(const char *label, int *v, int v_min, int v_max, const char *display_format = "%.0f"); -IMGUI_API bool SliderInt2(const char *label, int v[2], int v_min, int v_max, const char *display_format = "%.0f"); -IMGUI_API bool SliderInt3(const char *label, int v[3], int v_min, int v_max, const char *display_format = "%.0f"); -IMGUI_API bool SliderInt4(const char *label, int v[4], int v_min, int v_max, const char *display_format = "%.0f"); -IMGUI_API bool VSliderFloat(const char *label, const ImVec2 &size, float *v, float v_min, float v_max, const char *display_format = "%.3f", float power = 1.0f); -IMGUI_API bool VSliderInt(const char *label, const ImVec2 &size, int *v, int v_min, int v_max, const char *display_format = "%.0f"); - -// Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little colored preview square that can be left-clicked to open a picker, and right-clicked to open an option menu.) -// Note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can the pass the address of a first float element out of a contiguous structure, e.g. &myvector.x -IMGUI_API bool ColorEdit3(const char *label, float col[3], ImGuiColorEditFlags flags = 0); -IMGUI_API bool ColorEdit4(const char *label, float col[4], ImGuiColorEditFlags flags = 0); -IMGUI_API bool ColorPicker3(const char *label, float col[3], ImGuiColorEditFlags flags = 0); -IMGUI_API bool ColorPicker4(const char *label, float col[4], ImGuiColorEditFlags flags = 0, const float *ref_col = NULL); -IMGUI_API bool ColorButton(const char *desc_id, const ImVec4 &col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0)); // display a colored square/button, hover for details, return true when pressed. -IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. - -// Widgets: Trees -IMGUI_API bool TreeNode(const char *label); // if returning 'true' the node is open and the tree id is pushed into the id stack. user is responsible for calling TreePop(). -IMGUI_API bool TreeNode(const char *str_id, const char *fmt, ...) IM_FMTARGS(2); // read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). -IMGUI_API bool TreeNode(const void *ptr_id, const char *fmt, ...) IM_FMTARGS(2); // " -IMGUI_API bool TreeNodeV(const char *str_id, const char *fmt, va_list args) IM_FMTLIST(2); -IMGUI_API bool TreeNodeV(const void *ptr_id, const char *fmt, va_list args) IM_FMTLIST(2); -IMGUI_API bool TreeNodeEx(const char *label, ImGuiTreeNodeFlags flags = 0); -IMGUI_API bool TreeNodeEx(const char *str_id, ImGuiTreeNodeFlags flags, const char *fmt, ...) IM_FMTARGS(3); -IMGUI_API bool TreeNodeEx(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, ...) IM_FMTARGS(3); -IMGUI_API bool TreeNodeExV(const char *str_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) IM_FMTLIST(3); -IMGUI_API bool TreeNodeExV(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) IM_FMTLIST(3); -IMGUI_API void TreePush(const char *str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call Push/Pop yourself for layout purpose -IMGUI_API void TreePush(const void *ptr_id = NULL); // " -IMGUI_API void TreePop(); // ~ Unindent()+PopId() -IMGUI_API void TreeAdvanceToLabelPos(); // advance cursor x position by GetTreeNodeToLabelSpacing() -IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode -IMGUI_API void SetNextTreeNodeOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. -IMGUI_API bool CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). -IMGUI_API bool CollapsingHeader(const char *label, bool *p_open, ImGuiTreeNodeFlags flags = 0); // when 'p_open' isn't NULL, display an additional small close button on upper right of the header - -// Widgets: Selectable / Lists -IMGUI_API bool Selectable(const char *label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2 &size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height -IMGUI_API bool Selectable(const char *label, bool *p_selected, ImGuiSelectableFlags flags = 0, const ImVec2 &size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. -IMGUI_API bool ListBox(const char *label, int *current_item, const char *const items[], int items_count, int height_in_items = -1); -IMGUI_API bool ListBox(const char *label, int *current_item, bool (*items_getter)(void *data, int idx, const char **out_text), void *data, int items_count, int height_in_items = -1); -IMGUI_API bool ListBoxHeader(const char *label, const ImVec2 &size = ImVec2(0, 0)); // use if you want to reimplement ListBox() will custom data or interactions. make sure to call ListBoxFooter() afterwards. -IMGUI_API bool ListBoxHeader(const char *label, int items_count, int height_in_items = -1); // " -IMGUI_API void ListBoxFooter(); // terminate the scrolling region - -// Widgets: Value() Helpers. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) -IMGUI_API void Value(const char *prefix, bool b); -IMGUI_API void Value(const char *prefix, int v); -IMGUI_API void Value(const char *prefix, unsigned int v); -IMGUI_API void Value(const char *prefix, float v, const char *float_format = NULL); - -// Tooltips -IMGUI_API void SetTooltip(const char *fmt, ...) IM_FMTARGS(1); // set text tooltip under mouse-cursor, typically use with ImGui::IsItemHovered(). overidde any previous call to SetTooltip(). -IMGUI_API void SetTooltipV(const char *fmt, va_list args) IM_FMTLIST(1); -IMGUI_API void BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of contents). -IMGUI_API void EndTooltip(); - -// Menus -IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. -IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! -IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). -IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! -IMGUI_API bool BeginMenu(const char *label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! -IMGUI_API void EndMenu(); // only call EndBegin() if BeginMenu() returns true! -IMGUI_API bool MenuItem(const char *label, const char *shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. shortcuts are displayed for convenience but not processed by ImGui at the moment -IMGUI_API bool MenuItem(const char *label, const char *shortcut, bool *p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL - -// Popups -IMGUI_API void OpenPopup(const char *str_id); // call to mark popup as open (don't call every frame!). popups are closed when user click outside, or if CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. By default, Selectable()/MenuItem() are calling CloseCurrentPopup(). Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). -IMGUI_API bool BeginPopup(const char *str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. only call EndPopup() if BeginPopup() returns true! -IMGUI_API bool BeginPopupContextItem(const char *str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked on last item. if you can pass a NULL str_id only if the previous item had an id. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! -IMGUI_API bool BeginPopupContextWindow(const char *str_id = NULL, int mouse_button = 1, bool also_over_items = true); // helper to open and begin popup when clicked on current window. -IMGUI_API bool BeginPopupContextVoid(const char *str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked in void (where there are no imgui windows). -IMGUI_API bool BeginPopupModal(const char *name, bool *p_open = NULL, ImGuiWindowFlags flags = 0); // modal dialog (regular window with title bar, block interactions behind the modal window, can't close the modal window by clicking outside) -IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! -IMGUI_API bool OpenPopupOnItemClick(const char *str_id = NULL, int mouse_button = 1); // helper to open popup when clicked on last item. return true when just opened. -IMGUI_API bool IsPopupOpen(const char *str_id); // return true if the popup is open -IMGUI_API void CloseCurrentPopup(); // close the popup we have begin-ed into. clicking on a MenuItem or Selectable automatically close the current popup. - -// Logging/Capture: all text output from interface is captured to tty/file/clipboard. By default, tree nodes are automatically opened during logging. -IMGUI_API void LogToTTY(int max_depth = -1); // start logging to tty -IMGUI_API void LogToFile(int max_depth = -1, const char *filename = NULL); // start logging to file -IMGUI_API void LogToClipboard(int max_depth = -1); // start logging to OS clipboard -IMGUI_API void LogFinish(); // stop logging (close file, etc.) -IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard -IMGUI_API void LogText(const char *fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) - -// Drag and Drop -// [BETA API] Missing Demo code. API may evolve. -IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call when the current item is active. If this return true, you can call SetDragDropPayload() + EndDragDropSource() -IMGUI_API bool SetDragDropPayload(const char *type, const void *data, size_t size, ImGuiCond cond = 0); // type is a user defined string of maximum 12 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. -IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! -IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive an item. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() -IMGUI_API const ImGuiPayload *AcceptDragDropPayload(const char *type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. -IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! - -// Clipping -IMGUI_API void PushClipRect(const ImVec2 &clip_rect_min, const ImVec2 &clip_rect_max, bool intersect_with_current_clip_rect); -IMGUI_API void PopClipRect(); - -// Focus, Activation -// (Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHere()" when applicable, to make your code more forward compatible when navigation branch is merged) -IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. Please use instead of "if (IsWindowAppearing()) SetScrollHere()" to signify "default item". -IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. - -// Utilities -IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. -IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited- items that don't interact will always return false) -IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? -IMGUI_API bool IsItemClicked(int mouse_button = 0); // is the last item clicked? (e.g. button/node just clicked on) -IMGUI_API bool IsItemVisible(); // is the last item visible? (aka not out of sight due to clipping/scrolling.) -IMGUI_API bool IsAnyItemHovered(); -IMGUI_API bool IsAnyItemActive(); -IMGUI_API bool IsAnyItemFocused(); -IMGUI_API ImVec2 GetItemRectMin(); // get bounding rectangle of last item, in screen space -IMGUI_API ImVec2 GetItemRectMax(); // " -IMGUI_API ImVec2 GetItemRectSize(); // get size of last item, in screen space -IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. -IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags = 0); // is current window focused? or its root/child, depending on flags. see flags for options. -IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags = 0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. -IMGUI_API bool IsRectVisible(const ImVec2 &size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. -IMGUI_API bool IsRectVisible(const ImVec2 &rect_min, const ImVec2 &rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. -IMGUI_API float GetTime(); -IMGUI_API int GetFrameCount(); -IMGUI_API ImDrawList *GetOverlayDrawList(); // this draw list will be the last rendered one, useful to quickly draw overlays shapes/text -IMGUI_API ImDrawListSharedData *GetDrawListSharedData(); -IMGUI_API const char *GetStyleColorName(ImGuiCol idx); -IMGUI_API ImVec2 CalcTextSize(const char *text, const char *text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); -IMGUI_API void CalcListClipping(int items_count, float items_height, int *out_items_display_start, int *out_items_display_end); // calculate coarse clipping for large list of evenly sized items. Prefer using the ImGuiListClipper higher-level helper if you can. - -IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2 &size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame -IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) - -IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); -IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4 &in); -IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float &out_h, float &out_s, float &out_v); -IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float &out_r, float &out_g, float &out_b); - -// Inputs -IMGUI_API int GetKeyIndex(ImGuiKey imgui_key); // map ImGuiKey_* values into user's key index. == io.KeyMap[key] -IMGUI_API bool IsKeyDown(int user_key_index); // is key being held. == io.KeysDown[user_key_index]. note that imgui doesn't know the semantic of each entry of io.KeyDown[]. Use your own indices/enums according to how your backend/engine stored them into KeyDown[]! -IMGUI_API bool IsKeyPressed(int user_key_index, bool repeat = true); // was key pressed (went from !Down to Down). if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate -IMGUI_API bool IsKeyReleased(int user_key_index); // was key released (went from Down to !Down).. -IMGUI_API int GetKeyPressedAmount(int key_index, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate -IMGUI_API bool IsMouseDown(int button); // is mouse button held -IMGUI_API bool IsAnyMouseDown(); // is any mouse button held -IMGUI_API bool IsMouseClicked(int button, bool repeat = false); // did mouse button clicked (went from !Down to Down) -IMGUI_API bool IsMouseDoubleClicked(int button); // did mouse button double-clicked. a double-click returns false in IsMouseClicked(). uses io.MouseDoubleClickTime. -IMGUI_API bool IsMouseReleased(int button); // did mouse button released (went from Down to !Down) -IMGUI_API bool IsMouseDragging(int button = 0, float lock_threshold = -1.0f); // is mouse dragging. if lock_threshold < -1.0f uses io.MouseDraggingThreshold -IMGUI_API bool IsMouseHoveringRect(const ImVec2 &r_min, const ImVec2 &r_max, bool clip = true); // is mouse hovering given bounding rect (in screen space). clipped by current clipping settings. disregarding of consideration of focus/window ordering/blocked by a popup. -IMGUI_API bool IsMousePosValid(const ImVec2 *mouse_pos = NULL); // -IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls -IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve backup of mouse positioning at the time of opening popup we have BeginPopup() into -IMGUI_API ImVec2 GetMouseDragDelta(int button = 0, float lock_threshold = -1.0f); // dragging amount since clicking. if lock_threshold < -1.0f uses io.MouseDraggingThreshold -IMGUI_API void ResetMouseDragDelta(int button = 0); // -IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired cursor type, reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you -IMGUI_API void SetMouseCursor(ImGuiMouseCursor type); // set desired cursor type -IMGUI_API void CaptureKeyboardFromApp(bool capture = true); // manually override io.WantCaptureKeyboard flag next frame (said flag is entirely left for your application handle). e.g. force capture keyboard when your widget is being hovered. -IMGUI_API void CaptureMouseFromApp(bool capture = true); // manually override io.WantCaptureMouse flag next frame (said flag is entirely left for your application handle). - -// Clipboard Utilities (also see the LogToClipboard() function to capture or output text data to the clipboard) -IMGUI_API const char *GetClipboardText(); -IMGUI_API void SetClipboardText(const char *text); - -// Memory Utilities -// All those functions are not reliant on the current context. -// If you reload the contents of imgui.cpp at runtime, you may need to call SetCurrentContext() + SetAllocatorFunctions() again. -IMGUI_API void SetAllocatorFunctions(void *(*alloc_func)(size_t sz, void *user_data), void (*free_func)(void *ptr, void *user_data), void *user_data = NULL); -IMGUI_API void *MemAlloc(size_t size); -IMGUI_API void MemFree(void *ptr); - -} // namespace ImGui + // Context creation and access + // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for details. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); // access the ImGuiIO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) + IMGUI_API ImGuiPlatformIO& GetPlatformIO(); // access the ImGuiPlatformIO structure (mostly hooks/functions to connect to platform/renderer and OS Clipboard, IME etc.) + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleColor(), PushStyleVar() to modify style mid-frame! + IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! + IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. + + // Demo, Debug, Information + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. + IMGUI_API void ShowDebugLogWindow(bool* p_open = NULL); // create Debug Log window. display a simplified log of important dear imgui events. + IMGUI_API void ShowIDStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. + IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. + IMGUI_API void ShowFontSelector(const char* label); // add font selector block (not a window), essentially a combo listing the loaded fonts. + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as an end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.80 WIP" (essentially the value for IMGUI_VERSION from the compiled version of imgui.cpp) + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // new, recommended style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // best used with borders and a custom, thicker font + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style + + // Windows + // - Begin() = push window to the stack and start appending to it. End() = pop window from the stack. + // - Passing 'bool* p_open != NULL' shows a window-closing widget in the upper-right corner of the window, + // which clicking will set the boolean to false when clicked. + // - You may append multiple times to the same window during the same frame by calling Begin()/End() pairs multiple times. + // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). + // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + // - Note that the bottom of window stack always contains a window called "Debug". + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); + IMGUI_API void End(); + + // Child Windows + // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. + // - Before 1.90 (November 2023), the "ImGuiChildFlags child_flags = 0" parameter was "bool border = false". + // This API is backward compatible with old code, as we guarantee that ImGuiChildFlags_Borders == true. + // Consider updating your old code: + // BeginChild("Name", size, false) -> Begin("Name", size, 0); or Begin("Name", size, ImGuiChildFlags_None); + // BeginChild("Name", size, true) -> Begin("Name", size, ImGuiChildFlags_Borders); + // - Manual sizing (each axis can use a different setting e.g. ImVec2(0.0f, 400.0f)): + // == 0.0f: use remaining parent window size for this axis. + // > 0.0f: use specified size for this axis. + // < 0.0f: right/bottom-align to specified distance from available content boundaries. + // - Specifying ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY makes the sizing automatic based on child contents. + // Combining both ImGuiChildFlags_AutoResizeX _and_ ImGuiChildFlags_AutoResizeY defeats purpose of a scrolling region and is NOT recommended. + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API void EndChild(); + + // Windows Utilities + // - 'current window' = the window we are appending into while inside a Begin()/End() block. 'next window' = next window we will Begin() into. + IMGUI_API bool IsWindowAppearing(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. + IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API ImVec2 GetWindowSize(); // get current window size (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API float GetWindowWidth(); // get current window width (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().x. + IMGUI_API float GetWindowHeight(); // get current window height (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().y. + + // Window manipulation + // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use 0.0f or FLT_MAX if you don't want limits. Use -1 for both min and max of same axis to preserve current size (which itself is a constraint). Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() + IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // set next window scrolling value (use < 0.0f to not affect a given axis). + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowFontScale(float scale); // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / top-most. use NULL to remove focus. + + // Windows Scrolling + // - Any change of Scroll will be applied at the beginning of next frame in the first call to Begin(). + // - You may instead use SetNextWindowScroll() prior to calling Begin() to avoid this delay, as an alternative to using SetScrollX()/SetScrollY(). + IMGUI_API float GetScrollX(); // get scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.x - WindowSize.x - DecorationsSize.x + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.y - WindowSize.y - DecorationsSize.y + IMGUI_API void SetScrollHereX(float center_x_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_x_ratio=0.0: left, 0.5: center, 1.0: right. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollHereY(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + IMGUI_API void SetScrollFromPosY(float local_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); // modify a style color. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); // modify a style float variable. always use this if you modify the style after NewFrame()! + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); // modify a style ImVec2 variable. " + IMGUI_API void PushStyleVarX(ImGuiStyleVar idx, float val_x); // modify X component of a style ImVec2 variable. " + IMGUI_API void PushStyleVarY(ImGuiStyleVar idx, float val_y); // modify Y component of a style ImVec2 variable. " + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); // modify specified shared item flag, e.g. PushItemFlag(ImGuiItemFlags_NoTabStop, true) + IMGUI_API void PopItemFlag(); + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // push width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side). + IMGUI_API void PopItemWidth(); + IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side) + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position. NOT necessarily the width of last item unlike most 'Item' functions. + IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // push word-wrapping position for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + + // Style read access + // - Use the ShowStyleEditor() function to interactively see/edit the colors. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a white pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col, float alpha_mul = 1.0f); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. + + // Layout cursor positioning + // - By "cursor" we mean the current output position. + // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. + // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. + // - YOU CAN DO 99% OF WHAT YOU NEED WITH ONLY GetCursorScreenPos() and GetContentRegionAvail(). + // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: + // - Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. -> this is the preferred way forward. + // - Window-local coordinates: SameLine(offset), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), PushTextWrapPos() + // - Window-local coordinates: GetContentRegionMax(), GetWindowContentRegionMin(), GetWindowContentRegionMax() --> all obsoleted. YOU DON'T NEED THEM. + // - GetCursorScreenPos() = GetCursorPos() + GetWindowPos(). GetWindowPos() is almost only ever useful to convert from window-local to absolute coordinates. Try not to use it. + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND (prefer using this rather than GetCursorPos(), also more useful to work with ImDrawList API). + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetContentRegionAvail(); // available space from current position. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetCursorPos(); // [window-local] cursor position in window-local coordinates. This is not your best friend. + IMGUI_API float GetCursorPosX(); // [window-local] " + IMGUI_API float GetCursorPosY(); // [window-local] " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // [window-local] " + IMGUI_API void SetCursorPosX(float local_x); // [window-local] " + IMGUI_API void SetCursorPosY(float local_y); // [window-local] " + IMGUI_API ImVec2 GetCursorStartPos(); // [window-local] initial cursor position, in window-local coordinates. Call GetCursorScreenPos() after Begin() to get the absolute coordinates version. + + // Other layout functions + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. + IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in a horizontal-layout context. + IMGUI_API void Spacing(); // add vertical spacing. + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size. unlike InvisibleButton(), Dummy() won't take the mouse click or be navigable into. + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.com/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + IMGUI_API ImGuiID GetID(int int_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void SeparatorText(const char* label); // currently: formatted text with an horizontal line + + // Widgets: Main + // - Most widgets return true when the value has been changed or when pressed/selected + // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button + IMGUI_API bool SmallButton(const char* label); // button with (FramePadding.y == 0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, int* flags, int flags_value); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); // use with e.g. if (RadioButton("one", my_value==1)) { my_value = 1; } + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); // shortcut to handle the above pattern when value is an integer + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL); + IMGUI_API void Bullet(); // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + IMGUI_API bool TextLink(const char* label); // hyperlink text button, return true when clicked + IMGUI_API void TextLinkOpenURL(const char* label, const char* url = NULL); // hyperlink text button, automatically open file/url when clicked + + // Widgets: Images + // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. + // - Note that Image() may add +2.0f to provided size if a border is visible, ImageButton() adds style.FramePadding*2.0f to provided size. + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + // Widgets: Combo Box (Dropdown) + // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. + // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. This is analogous to how ListBox are created. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drag Sliders + // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every function, note that a 'float v[X]' function argument is the same as 'float* v', + // the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits if ImGuiSliderFlags_AlwaysClamp is not used. + // - Use v_max = FLT_MAX / INT_MAX etc to avoid clamping to a maximum, same with v_min = -FLT_MAX / INT_MIN to avoid clamping to a minimum. + // - We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. + // - Legacy: Pre-1.78 there are DragXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Regular Sliders + // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Legacy: Pre-1.78 there are SliderXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Input with Keyboard + // - If you want to use InputText() with std::string or any custom dynamic string type, see misc/cpp/imgui_stdlib.h and comments in imgui_demo.cpp. + // - Most of the ImGuiInputTextFlags flags are only useful for InputText() and not for InputFloatX, InputIntX, InputDouble etc. + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little color square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // - Note that in C++ a 'float v[X]' function argument is the _same_ as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. + // - You can pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // display a color square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + // - TreeNode functions return true when the node is open, in which case you need to also call TreePop() when you are finished displaying the tree node contents. + IMGUI_API bool TreeNode(const char* label); + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // helper variation to easily decorelate the id from the displayed string. Read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushID(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const void* ptr_id); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopID() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. + IMGUI_API void SetNextItemOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + IMGUI_API void SetNextItemStorageID(ImGuiID storage_id); // set id to use for open/close storage (default to same as item id). + + // Widgets: Selectables + // - A selectable highlights when hovered, and can display another color when selected. + // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. This is so a series of selected Selectable appear contiguous. + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + + // Multi-selection system for Selectable(), Checkbox(), TreeNode() functions [BETA] + // - This enables standard multi-selection/range-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc.) in a way that also allow a clipper to be used. + // - ImGuiSelectionUserData is often used to store your item index within the current view (but may store something else). + // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. + // - TreeNode() is technically supported but... using this correctly is more complicated. You need some sort of linear/random access to your tree, + // which is suited to advanced trees setups already implementing filters and clipper. We will work simplifying the current demo. + // - 'selection_size' and 'items_count' parameters are optional and used by a few features. If they are costly for you to compute, you may avoid them. + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1); + IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); + IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); + IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. + + // Widgets: List Boxes + // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items. + // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. + // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth + // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items + IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region + IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items = -1); + + // Widgets: Data Plotting + // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + + // Widgets: Value() Helpers. + // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Widgets: Menus + // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. + // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. + // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. + // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndMenu() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Tooltips + // - Tooltips are windows following the mouse. They do not take focus away. + // - A tooltip window can contain items of any types. + // - SetTooltip() is more or less a shortcut for the 'if (BeginTooltip()) { Text(...); EndTooltip(); }' idiom (with a subtlety that it discard any previously submitted tooltip) + IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. + IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip()/BeginItemTooltip() returns true! + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip. Often used after a ImGui::IsItemHovered() check. Override any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Tooltips: helpers for showing a tooltip when hovering an item + // - BeginItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip) && BeginTooltip())' idiom. + // - SetItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { SetTooltip(...); }' idiom. + // - Where 'ImGuiHoveredFlags_ForTooltip' itself is a shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on active input type. For mouse it defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + IMGUI_API bool BeginItemTooltip(); // begin/append a tooltip window if preceding item was hovered. + IMGUI_API void SetItemTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip if preceding item was hovered. override any previous call to SetTooltip(). + IMGUI_API void SetItemTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Popups, Modals + // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. + // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. + // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). + // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. + // This is sometimes leading to confusing mistakes. May rework this in the future. + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards if returned true. ImGuiWindowFlags are forwarded to the window. + // - BeginPopupModal(): block every interaction behind the window, cannot be closed by user, add a dimming background, has a title bar. + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + + // Popups: open/close functions + // - OpenPopup(): set popup state to open. ImGuiPopupFlags are available for opening options. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - CloseCurrentPopup(): use inside the BeginPopup()/EndPopup() scope to close manually. + // - CloseCurrentPopup() is called by default by Selectable()/MenuItem() when activated (FIXME: need some options). + // - Use ImGuiPopupFlags_NoOpenOverExistingPopup to avoid opening a popup if there's already one at the same level. This is equivalent to e.g. testing for !IsAnyPopupOpen() prior to OpenPopup(). + // - Use IsWindowAppearing() after BeginPopup() to tell if a window just opened. + // - IMPORTANT: Notice that for OpenPopupOnItemClick() we exceptionally default flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter + IMGUI_API void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags = 0); // call to mark popup as open (don't call every frame!). + IMGUI_API void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags = 0); // id overload to facilitate calling from nested stacks + IMGUI_API void OpenPopupOnItemClick(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // helper to open popup when clicked on last item. Default to ImGuiPopupFlags_MouseButtonRight == 1. (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors) + IMGUI_API void CloseCurrentPopup(); // manually close the popup we have begin-ed into. + + // Popups: open+begin combined functions helpers + // - Helpers to do OpenPopup+BeginPopup where the Open action is triggered by e.g. hovering an item and right-clicking. + // - They are convenient to easily create context menus, hence the name. + // - IMPORTANT: Notice that BeginPopupContextXXX takes ImGuiPopupFlags just like OpenPopup() and unlike BeginPopup(). For full consistency, we may add ImGuiWindowFlags to the BeginPopupContextXXX functions in the future. + // - IMPORTANT: Notice that we exceptionally default their flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter, so if you add other flags remember to re-add the ImGuiPopupFlags_MouseButtonRight. + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked on last item. Use str_id==NULL to associate the popup to previous item. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1);// open+begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked in void (where there are no windows). + + // Popups: query functions + // - IsPopupOpen(): return true if the popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId: return true if any popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel: return true if any popup is open. + IMGUI_API bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags = 0); // return true if the popup is open. + + // Tables + // - Full-featured replacement for old Columns API. + // - See Demo->Tables for demo code. See top of imgui_tables.cpp for general commentary. + // - See ImGuiTableFlags_ and ImGuiTableColumnFlags_ enums for a description of available flags. + // The typical call flow is: + // - 1. Call BeginTable(), early out if returning false. + // - 2. Optionally call TableSetupColumn() to submit column name/flags/defaults. + // - 3. Optionally call TableSetupScrollFreeze() to request scroll freezing of columns/rows. + // - 4. Optionally call TableHeadersRow() to submit a header row. Names are pulled from TableSetupColumn() data. + // - 5. Populate contents: + // - In most situations you can use TableNextRow() + TableSetColumnIndex(N) to start appending into a column. + // - If you are using tables as a sort of grid, where every column is holding the same type of contents, + // you may prefer using TableNextColumn() instead of TableNextRow() + TableSetColumnIndex(). + // TableNextColumn() will automatically wrap-around into the next row if needed. + // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! + // - Summary of possible call flow: + // - TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // - TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // - TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // - TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! + // - 5. Call EndTable() + IMGUI_API bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextColumn(); // append into the next column (or first column of next row if currently in last column). Return true when column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true when column is visible. + + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify label, resizing policy, default width/weight, id, various other flags etc. + // - Use TableHeadersRow() to create a header row and automatically submit a TableHeader() for each column. + // Headers are required to perform: reordering, sorting, and opening the context menu. + // The context menu can also be made available in columns body using ImGuiTableFlags_ContextMenuInBody. + // - You may manually submit headers using TableNextRow() + TableHeader() calls, but this is only useful in + // some advanced use cases (e.g. adding custom widgets in header row). + // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); + IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + IMGUI_API void TableHeadersRow(); // submit a row with headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableAngledHeadersRow(); // submit a row with angled headers for every column with the ImGuiTableColumnFlags_AngledHeader flag. MUST BE FIRST ROW. + + // Tables: Sorting & Miscellaneous functions + // - Sorting: call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. + // When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have + // changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, + // else you may wastefully sort your data every frame! + // - Functions args 'int column_n' treat the default value of -1 as the same as passing the current column index. + IMGUI_API ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable(). + IMGUI_API int TableGetColumnCount(); // return number of columns (value passed to BeginTable) + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API int TableGetRowIndex(); // return current row index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column. + IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1); // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column. + IMGUI_API void TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) + IMGUI_API int TableGetHoveredColumn(); // return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. Can also use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. + IMGUI_API void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1); // change the color of a cell, row, or column. See ImGuiTableBgTarget_ flags for details. + + // Legacy Columns API (prefer using Tables!) + // - You can also use SameLine(pos_x) to mimic simplified columns. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool borders = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // Tab Bars, Tabs + // - Note: Tabs are automatically created by the docking system (when in 'docking' branch). Use this to create tab bars/tabs yourself. + IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar + IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. + IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. + IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + + // Logging/Capture + // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) + IMGUI_API void LogToFile(int auto_open_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int auto_open_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + IMGUI_API void LogTextV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Drag and Drop + // - On source items, call BeginDragDropSource(), if it returns true also call SetDragDropPayload() + EndDragDropSource(). + // - On target candidates, call BeginDragDropTarget(), if it returns true also call AcceptDragDropPayload() + EndDragDropTarget(). + // - If you stop calling BeginDragDropSource() the payload is preserved however it won't have a preview tooltip (we currently display a fallback "..." tooltip, see #1725) + // - An item can be both drag source and drop target. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call after submitting an item which may be dragged. when this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. Return true when payload has been accepted. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. returns NULL when drag and drop is finished or inactive. use ImGuiPayload::IsDataType() to test for the payload type. + + // Disabling [BETA API] + // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) + // - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) + // - Tooltips windows by exception are opted out of disabling. + // - BeginDisabled(false) essentially does nothing useful but is provided to facilitate use of boolean expressions. If you can avoid calling BeginDisabled(False)/EndDisabled() best to avoid it. + IMGUI_API void BeginDisabled(bool disabled = true); + IMGUI_API void EndDisabled(); + + // Clipping + // - Mouse hovering is affected by ImGui::PushClipRect() calls, unlike direct calls to ImDrawList::PushClipRect() which are render only. + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + // - Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHereY()" when applicable to signify "this is the default item" + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Overlapping mode + IMGUI_API void SetNextItemAllowOverlap(); // allow next item to be overlapped by a subsequent item. Useful with invisible buttons, selectable, treenode covering an area where subsequent items may need to be added. Note that both Selectable() and TreeNode() have dedicated flags doing this. + + // Item/Widgets Utilities and Query Functions + // - Most of the functions are referring to the previous Item that has been submitted. + // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited. This will continuously return true while holding mouse button on an item. Items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(ImGuiMouseButton mouse_button = 0); // is the last item hovered and mouse clicked on? (**) == IsMouseClicked(mouse_button) && IsItemHovered()Important. (**) this is NOT equivalent to the behavior of e.g. Button(). Read comments in function definition. + IMGUI_API bool IsItemVisible(); // is the last item visible? (items may be out of sight because of clipping/scrolling) + IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. + IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). + IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing. + IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode(). + IMGUI_API bool IsAnyItemHovered(); // is any item hovered? + IMGUI_API bool IsAnyItemActive(); // is any item active? + IMGUI_API bool IsAnyItemFocused(); // is any item focused? + IMGUI_API ImGuiID GetItemID(); // get ID of last item (~~ often same ImGui::GetID(label) beforehand) + IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item + + // Viewports + // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. + // - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. + // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. + IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. + + // Background/Foreground Draw Lists + IMGUI_API ImDrawList* GetBackgroundDrawList(); // this draw list will be the first rendered one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(); // this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + + // Miscellaneous Utilities + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API double GetTime(); // get global imgui time. incremented by io.DeltaTime every frame. + IMGUI_API int GetFrameCount(); // get global imgui frame count. incremented by 1 every frame. + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); // you may use this when creating your own ImDrawList instances. + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). + IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + + // Text Utilities + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + + // Color Utilities + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs Utilities: Keyboard/Mouse/Gamepad + // - the ImGuiKey enum contains all possible keyboard, mouse and gamepad inputs (e.g. ImGuiKey_A, ImGuiKey_MouseLeft, ImGuiKey_GamepadDpadUp...). + // - before v1.87, we used ImGuiKey to carry native/user indices as defined by each backends. About use of those legacy ImGuiKey values: + // - without IMGUI_DISABLE_OBSOLETE_KEYIO (legacy support): you can still use your legacy native/user indices (< 512) according to how your backend/engine stored them in io.KeysDown[], but need to cast them to ImGuiKey. + // - with IMGUI_DISABLE_OBSOLETE_KEYIO (this is the way forward): any use of ImGuiKey will assert with key < 512. GetKeyIndex() is pass-through and therefore deprecated (gone if IMGUI_DISABLE_OBSOLETE_KEYIO is defined). + IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. + IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord); // was key chord (mods + key) pressed, e.g. you can pass 'ImGuiMod_Ctrl | ImGuiKey_S' as a key-chord. This doesn't do any routing or focus check, please consider using Shortcut() function instead. + IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names a provided for debugging purpose and are not meant to be saved persistently not compared. + IMGUI_API void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); // Override io.WantCaptureKeyboard flag next frame (said flag is left for your application to handle, typically when true it instructs your app to ignore inputs). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard"; after the next NewFrame() call. + + // Inputs Utilities: Shortcut Testing & Routing [BETA] + // - ImGuiKeyChord = a ImGuiKey + optional ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super. + // ImGuiKey_C // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments) + // ImGuiMod_Ctrl | ImGuiKey_C // Accepted by functions taking ImGuiKeyChord arguments) + // only ImGuiMod_XXX values are legal to combine with an ImGuiKey. You CANNOT combine two ImGuiKey values. + // - The general idea is that several callers may register interest in a shortcut, and only one owner gets it. + // Parent -> call Shortcut(Ctrl+S) // When Parent is focused, Parent gets the shortcut. + // Child1 -> call Shortcut(Ctrl+S) // When Child1 is focused, Child1 gets the shortcut (Child1 overrides Parent shortcuts) + // Child2 -> no call // When Child2 is focused, Parent gets the shortcut. + // The whole system is order independent, so if Child1 makes its calls before Parent, results will be identical. + // This is an important property as it facilitate working with foreign code or larger codebase. + // - To understand the difference: + // - IsKeyChordPressed() compares mods and call IsKeyPressed() -> function has no side-effect. + // - Shortcut() submits a route, routes are resolved, if it currently can be routed it calls IsKeyChordPressed() -> function has (desirable) side-effects as it can prevents another call from getting the route. + // - Visualize registered routes in 'Metrics/Debugger->Inputs'. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + IMGUI_API void SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + + // Inputs Utilities: Key/Input Ownership [BETA] + // - One common use case would be to allow your items to disable standard inputs behaviors such + // as Tab or Alt key handling, Mouse Wheel scrolling, etc. + // e.g. Button(...); SetItemKeyOwner(ImGuiKey_MouseWheelY); to make hovering/activating a button disable wheel for scrolling. + // - Reminder ImGuiKey enum include access to mouse buttons and gamepad, so key ownership can apply to them. + // - Many related features are still in imgui_internal.h. For instance, most IsKeyXXX()/IsMouseXXX() functions have an owner-id-aware version. + IMGUI_API void SetItemKeyOwner(ImGuiKey key); // Set key owner to last item ID if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + + // Inputs Utilities: Mouse specific + // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. + // - You can also use regular integer: it is forever guaranteed that 0=Left, 1=Right, 2=Middle. + // - Dragging operations are only reported after mouse has moved a certain distance away from the initial clicking position (see 'lock_threshold' and 'io.MouseDraggingThreshold') + IMGUI_API bool IsMouseDown(ImGuiMouseButton button); // is mouse button held? + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) + IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available + IMGUI_API bool IsAnyMouseDown(); // [WILL OBSOLETE] is any mouse button held? This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve mouse position at the time of opening popup we have BeginPopup() into (helper to avoid user backing that value themselves) + IMGUI_API bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold = -1.0f); // is mouse dragging? (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API ImVec2 GetMouseDragDelta(ImGuiMouseButton button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API void ResetMouseDragDelta(ImGuiMouseButton button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired mouse cursor shape. Important: reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor cursor_type); // set desired mouse cursor shape + IMGUI_API void SetNextFrameWantCaptureMouse(bool want_capture_mouse); // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instucts your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call. + + // Clipboard Utilities + // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard. + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Settings/.Ini Utilities + // - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini"). + // - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually. + // - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables). + IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename). + IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source. + IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext). + IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. + + // Debug Utilities + // - Your main debugging friend is the ShowMetricsWindow() function, which is also accessible from Demo->Tools->Metrics Debugger + IMGUI_API void DebugTextEncoding(const char* text); + IMGUI_API void DebugFlashStyleColor(ImGuiCol idx); + IMGUI_API void DebugStartItemPicker(); + IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + IMGUI_API void DebugLog(const char* fmt, ...) IM_FMTARGS(1); // Call via IMGUI_DEBUG_LOG() for maximum stripping in caller code! + IMGUI_API void DebugLogV(const char* fmt, va_list args) IM_FMTLIST(1); +#endif + + // Memory Allocators + // - Those functions are not reliant on the current context. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. + IMGUI_API void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data = NULL); + IMGUI_API void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + +} // namespace ImGui + +//----------------------------------------------------------------------------- +// [SECTION] Flags & Enumerations +//----------------------------------------------------------------------------- // Flags for ImGui::Begin() +// (Those are per-window flags. There are shared flags in ImGuiIO: io.ConfigWindowsResizeFromEdges and io.ConfigWindowsMoveFromTitleBarOnly) enum ImGuiWindowFlags_ { - ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar - ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip - ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window - ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programatically) - ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. - ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it - ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame - // ImGuiWindowFlags_ShowBorders = 1 << 7, // Show borders around windows and items (OBSOLETE! Use e.g. style.FrameBorderSize=1.0f to enable borders). - ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file - ImGuiWindowFlags_NoInputs = 1 << 9, // Disable catching mouse or keyboard inputs, hovering test with pass through. - ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar - ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. - ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state - ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programatically giving it focus) - ImGuiWindowFlags_AlwaysVerticalScrollbar = 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) - ImGuiWindowFlags_AlwaysHorizontalScrollbar = 1 << 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) - ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) - ImGuiWindowFlags_ResizeFromAnySide = 1 << 17, // (WIP) Enable resize from any corners and borders. Your back-end needs to honor the different values of io.MouseCursor set by imgui. - ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window - ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) - ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, - - // [Internal] - ImGuiWindowFlags_NavFlattened = 1 << 23, // (WIP) Allow gamepad/keyboard navigation to cross over parent border to this child (only use on child that have no scrolling!) - ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() - ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() - ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() - ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() - ImGuiWindowFlags_ChildMenu = 1 << 28 // Don't use! For internal use by BeginMenu() + ImGuiWindowFlags_None = 0, + ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar + ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip + ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window + ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programmatically) + ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it. Also referred to as Window Menu Button (e.g. within a docking node). + ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame + ImGuiWindowFlags_NoBackground = 1 << 7, // Disable drawing background color (WindowBg, etc.) and outside border. Similar as using SetNextWindowBgAlpha(0.0f). + ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file + ImGuiWindowFlags_NoMouseInputs = 1 << 9, // Disable catching mouse, hovering test with pass through. + ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar + ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. + ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state + ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) + ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) + ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) + ImGuiWindowFlags_NoNavInputs = 1 << 16, // No gamepad/keyboard navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 17, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 18, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, + ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + + // [Internal] + ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() + ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() + ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() + ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() + ImGuiWindowFlags_ChildMenu = 1 << 28, // Don't use! For internal use by BeginMenu() + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 30, // Obsoleted in 1.90.0: Use ImGuiChildFlags_AlwaysUseWindowPadding in BeginChild() call. + ImGuiWindowFlags_NavFlattened = 1 << 31, // Obsoleted in 1.90.9: Use ImGuiChildFlags_NavFlattened in BeginChild() call. +#endif +}; + +// Flags for ImGui::BeginChild() +// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'. +// About using AutoResizeX/AutoResizeY flags: +// - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints"). +// - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing. +// - This allows BeginChild() to return false when not within boundaries (e.g. when scrolling), which is more optimal. BUT it won't update its auto-size while clipped. +// While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional "resizing after becoming visible again" glitch. +// - You may also use ImGuiChildFlags_AlwaysAutoResize to force an update even when child window is not in view. +// HOWEVER PLEASE UNDERSTAND THAT DOING SO WILL PREVENT BeginChild() FROM EVER RETURNING FALSE, disabling benefits of coarse clipping. +enum ImGuiChildFlags_ +{ + ImGuiChildFlags_None = 0, + ImGuiChildFlags_Borders = 1 << 0, // Show an outer border and enable WindowPadding. (IMPORTANT: this is always == 1 == true for legacy reason) + ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, // Pad with style.WindowPadding even if no border are drawn (no padding by default for non-bordered child windows because it makes more sense) + ImGuiChildFlags_ResizeX = 1 << 2, // Allow resize from right border (layout direction). Enable .ini saving (unless ImGuiWindowFlags_NoSavedSettings passed to window flags) + ImGuiChildFlags_ResizeY = 1 << 3, // Allow resize from bottom border (layout direction). " + ImGuiChildFlags_AutoResizeX = 1 << 4, // Enable auto-resizing width. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AutoResizeY = 1 << 5, // Enable auto-resizing height. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AlwaysAutoResize = 1 << 6, // Combined with AutoResizeX/AutoResizeY. Always measure size even when child is hidden, always return true, always disable clipping optimization! NOT RECOMMENDED. + ImGuiChildFlags_FrameStyle = 1 << 7, // Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding. + ImGuiChildFlags_NavFlattened = 1 << 8, // [BETA] Share focus scope, allow gamepad/keyboard navigation to cross over parent border to this child or between sibling child windows. + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiChildFlags_Border = ImGuiChildFlags_Borders, // Renamed in 1.91.1 (August 2024) for consistency. +#endif +}; + +// Flags for ImGui::PushItemFlag() +// (Those are shared by all items) +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_None = 0, // (Default) + ImGuiItemFlags_NoTabStop = 1 << 0, // false // Disable keyboard tabbing. This is a "lighter" version of ImGuiItemFlags_NoNav. + ImGuiItemFlags_NoNav = 1 << 1, // false // Disable any form of focusing (keyboard/gamepad directional navigation and SetKeyboardFocusHere() calls). + ImGuiItemFlags_NoNavDefaultFocus = 1 << 2, // false // Disable item being a candidate for default focus (e.g. used by title bar items). + ImGuiItemFlags_ButtonRepeat = 1 << 3, // false // Any button-like behavior will have repeat mode enabled (based on io.KeyRepeatDelay and io.KeyRepeatRate values). Note that you can also call IsItemActive() after any button to tell if it is being held. + ImGuiItemFlags_AutoClosePopups = 1 << 4, // true // MenuItem()/Selectable() automatically close their parent popup window. }; // Flags for ImGui::InputText() +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigInputTextCursorBlink and io.ConfigInputTextEnterKeepActive) enum ImGuiInputTextFlags_ { - ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ - ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef - ImGuiInputTextFlags_CharsUppercase = 1 << 2, // Turn a..z into A..Z - ImGuiInputTextFlags_CharsNoBlank = 1 << 3, // Filter out spaces, tabs - ImGuiInputTextFlags_AutoSelectAll = 1 << 4, // Select entire text when first taking mouse focus - ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, // Return 'true' when Enter is pressed (as opposed to when the value was modified) - ImGuiInputTextFlags_CallbackCompletion = 1 << 6, // Call user function on pressing TAB (for completion handling) - ImGuiInputTextFlags_CallbackHistory = 1 << 7, // Call user function on pressing Up/Down arrows (for history handling) - ImGuiInputTextFlags_CallbackAlways = 1 << 8, // Call user function every time. User code may query cursor position, modify text buffer. - ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, // Call user function to filter character. Modify data->EventChar to replace/filter input, or return 1 to discard character. - ImGuiInputTextFlags_AllowTabInput = 1 << 10, // Pressing TAB input a '\t' character into the text field - ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, // In multi-line mode, unfocus with Enter, add new line with Ctrl+Enter (default is opposite: unfocus with Ctrl+Enter, add line with Enter). - ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, // Disable following the cursor horizontally - ImGuiInputTextFlags_AlwaysInsertMode = 1 << 13, // Insert mode - ImGuiInputTextFlags_ReadOnly = 1 << 14, // Read-only mode - ImGuiInputTextFlags_Password = 1 << 15, // Password mode, display all characters as '*' - ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). - // [Internal] - ImGuiInputTextFlags_Multiline = 1 << 20 // For internal use by InputTextMultiline() + // Basic filters (also see ImGuiInputTextFlags_CallbackCharFilter) + ImGuiInputTextFlags_None = 0, + ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ + ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef + ImGuiInputTextFlags_CharsScientific = 1 << 2, // Allow 0123456789.+-*/eE (Scientific notation input) + ImGuiInputTextFlags_CharsUppercase = 1 << 3, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 4, // Filter out spaces, tabs + + // Inputs + ImGuiInputTextFlags_AllowTabInput = 1 << 5, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 6, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider looking at the IsItemDeactivatedAfterEdit() function. + ImGuiInputTextFlags_EscapeClearsAll = 1 << 7, // Escape key clears content if not empty, and deactivate otherwise (contrast to default behavior of Escape to revert) + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 8, // In multi-line mode, validate with Enter, add new line with Ctrl+Enter (default is opposite: validate with Ctrl+Enter, add line with Enter). + + // Other options + ImGuiInputTextFlags_ReadOnly = 1 << 9, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 10, // Password mode, display all characters as '*', disable copy + ImGuiInputTextFlags_AlwaysOverwrite = 1 << 11, // Overwrite mode + ImGuiInputTextFlags_AutoSelectAll = 1 << 12, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_ParseEmptyRefVal = 1 << 13, // InputFloat(), InputInt(), InputScalar() etc. only: parse empty string as zero value. + ImGuiInputTextFlags_DisplayEmptyRefVal = 1 << 14, // InputFloat(), InputInt(), InputScalar() etc. only: when value is zero, do not display it. Generally used with ImGuiInputTextFlags_ParseEmptyRefVal. + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 15, // Disable following the cursor horizontally + ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + + // Callback features + ImGuiInputTextFlags_CallbackCompletion = 1 << 17, // Callback on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 18, // Callback on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 19, // Callback on each iteration. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 20, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. + ImGuiInputTextFlags_CallbackResize = 1 << 21, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) + ImGuiInputTextFlags_CallbackEdit = 1 << 22, // Callback on any edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) + + // Obsolete names + //ImGuiInputTextFlags_AlwaysInsertMode = ImGuiInputTextFlags_AlwaysOverwrite // [renamed in 1.82] name was not matching behavior }; // Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() enum ImGuiTreeNodeFlags_ { - ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected - ImGuiTreeNodeFlags_Framed = 1 << 1, // Full colored frame (e.g. for CollapsingHeader) - ImGuiTreeNodeFlags_AllowItemOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one - ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack - ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) - ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open - ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node - ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. - ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). - ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow - ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). - // ImGuITreeNodeFlags_SpanAllAvailWidth = 1 << 11, // FIXME: TODO: Extend hit box horizontally even if not framed - // ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 12, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) - ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoAutoOpenOnLog - -// Obsolete names (will be removed) + ImGuiTreeNodeFlags_None = 0, + ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected + ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack + ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) + ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Open on double-click instead of simple click (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Open when clicking on the arrow part (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. + ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow. IMPORTANT: node can still be marked open/close if you don't set the _Leaf flag! + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding() before the node. + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line without using AllowOverlap mode. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (cover the indent area). + ImGuiTreeNodeFlags_SpanTextWidth = 1 << 13, // Narrow hit box + narrow hovering highlight, will only cover the label text. + ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, // Frame will span all columns of its container table (text will still fit in current column) + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 15, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 16, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, + #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , - ImGuiTreeNodeFlags_AllowOverlapMode = ImGuiTreeNodeFlags_AllowItemOverlap + ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 #endif }; +// Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument instead of 'ImGuiPopupFlags flags', +// we need to treat small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. +// - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. +// IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter +// and want to use another flag, you need to pass in the ImGuiPopupFlags_MouseButtonRight flag explicitly. +// - Multiple buttons currently cannot be combined/or-ed in those functions (we could allow it later). +enum ImGuiPopupFlags_ +{ + ImGuiPopupFlags_None = 0, + ImGuiPopupFlags_MouseButtonLeft = 0, // For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left) + ImGuiPopupFlags_MouseButtonRight = 1, // For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right) + ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) + ImGuiPopupFlags_MouseButtonMask_ = 0x1F, + ImGuiPopupFlags_MouseButtonDefault_ = 1, + ImGuiPopupFlags_NoReopen = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't reopen same popup if already open (won't reposition, won't reinitialize navigation) + //ImGuiPopupFlags_NoReopenAlwaysNavInit = 1 << 6, // For OpenPopup*(), BeginPopupContext*(): focus and initialize navigation even when not reopening. + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 8, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 10, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 11, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, +}; + // Flags for ImGui::Selectable() enum ImGuiSelectableFlags_ { - ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this don't close parent popup window - ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) - ImGuiSelectableFlags_AllowDoubleClick = 1 << 2 // Generate press events on double clicks too + ImGuiSelectableFlags_None = 0, + ImGuiSelectableFlags_NoAutoClosePopups = 1 << 0, // Clicking this doesn't close parent popup window (overrides ImGuiItemFlags_AutoClosePopups) + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Frame will span all columns of its container table (text will still fit in current column) + ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too + ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text + ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + ImGuiSelectableFlags_Highlight = 1 << 5, // Make the item be displayed as if it is hovered + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiSelectableFlags_DontClosePopups = ImGuiSelectableFlags_NoAutoClosePopups, // Renamed in 1.91.0 + ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap, // Renamed in 1.89.7 +#endif }; // Flags for ImGui::BeginCombo() enum ImGuiComboFlags_ { - ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default - ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() - ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) - ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible - ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible - ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest + ImGuiComboFlags_None = 0, + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button + ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button + ImGuiComboFlags_WidthFitPreview = 1 << 7, // Width dynamically calculated from preview contents + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, +}; + +// Flags for ImGui::BeginTabBar() +enum ImGuiTabBarFlags_ +{ + ImGuiTabBarFlags_None = 0, + ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list + ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear + ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) + ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab + ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab + ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 7, // Resize tabs when they don't fit + ImGuiTabBarFlags_FittingPolicyScroll = 1 << 8, // Add scroll buttons when tabs don't fit + ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll, + ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown, +}; + +// Flags for ImGui::BeginTabItem() +enum ImGuiTabItemFlags_ +{ + ImGuiTabItemFlags_None = 0, + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + set ImGuiTabItemFlags_NoAssumedClosure. + ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID()/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) + ImGuiTabItemFlags_Trailing = 1 << 7, // Enforce the tab position to the right of the tab bar (before the scrolling buttons) + ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, // Tab is selected when trying to close + closure is not immediately assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. }; // Flags for ImGui::IsWindowFocused() enum ImGuiFocusedFlags_ { - ImGuiFocusedFlags_ChildWindows = 1 << 0, // IsWindowFocused(): Return true if any children of the window is focused - ImGuiFocusedFlags_RootWindow = 1 << 1, // IsWindowFocused(): Test from root window (top most parent of the current hierarchy) - ImGuiFocusedFlags_AnyWindow = 1 << 2, // IsWindowFocused(): Return true if any window is focused - ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows + ImGuiFocusedFlags_None = 0, + ImGuiFocusedFlags_ChildWindows = 1 << 0, // Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! + ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, }; // Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() +// Note: if you are trying to check whether your mouse should be dispatched to Dear ImGui or to your app, you should use 'io.WantCaptureMouse' instead! Please read the FAQ! +// Note: windows with the ImGuiWindowFlags_NoInputs flag are ignored by IsWindowHovered() calls. enum ImGuiHoveredFlags_ { - ImGuiHoveredFlags_Default = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. - ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered - ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) - ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered - ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 3, // Return true even if a popup window is normally blocking access to this item/window - // ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 4, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 5, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. - ImGuiHoveredFlags_AllowWhenOverlapped = 1 << 6, // Return true even if the position is overlapped by another window - ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, - ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows + ImGuiHoveredFlags_None = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. + ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered + ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) + ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered + ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, // IsItemHovered() only: Return true even if the item uses AllowOverlap mode and is overlapped by another hoverable item. + ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window. + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_NoNavOverride = 1 << 11, // IsItemHovered() only: Disable using gamepad/keyboard navigation state when active, always query mouse + ImGuiHoveredFlags_AllowWhenOverlapped = ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenOverlappedByWindow, + ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, + + // Tooltips mode + // - typically used in IsItemHovered() + SetTooltip() sequence. + // - this is a shortcut to pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' where you can reconfigure desired behavior. + // e.g. 'TooltipHoveredFlagsForMouse' defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + // - for frequently actioned or hovered items providing a tooltip, you want may to use ImGuiHoveredFlags_ForTooltip (stationary + delay) so the tooltip doesn't show too often. + // - for items which main purpose is to be hovered, or items with low affordance, or in less consistent apps, prefer no delay or shorter delay. + ImGuiHoveredFlags_ForTooltip = 1 << 12, // Shortcut for standard flags when using IsItemHovered() + SetTooltip() sequence. + + // (Advanced) Mouse Hovering delays. + // - generally you can use ImGuiHoveredFlags_ForTooltip to use application-standardized flags. + // - use those if you need specific overrides. + ImGuiHoveredFlags_Stationary = 1 << 13, // Require mouse to be stationary for style.HoverStationaryDelay (~0.15 sec) _at least one time_. After this, can move on same item/window. Using the stationary test tends to reduces the need for a long delay. + ImGuiHoveredFlags_DelayNone = 1 << 14, // IsItemHovered() only: Return true immediately (default). As this is the default you generally ignore this. + ImGuiHoveredFlags_DelayShort = 1 << 15, // IsItemHovered() only: Return true after style.HoverDelayShort elapsed (~0.15 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_DelayNormal = 1 << 16, // IsItemHovered() only: Return true after style.HoverDelayNormal elapsed (~0.40 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_NoSharedDelay = 1 << 17, // IsItemHovered() only: Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) }; // Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() enum ImGuiDragDropFlags_ { - // BeginDragDropSource() flags - ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior. - ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return true, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item. - ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. - ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. - ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. - // AcceptDragDropPayload() flags - ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. - ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. - ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect // For peeking ahead and inspecting the payload before delivery. -}; - -// Standard Drag and Drop payload types. You can define you own payload types using 12-characters long strings. Types starting with '_' are defined by Dear ImGui. -#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3] // Standard type for colors, without alpha. User code may use this type. -#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4] // Standard type for colors. User code may use this type. - -// User fill ImGuiIO.KeyMap[] array with indices into the ImGuiIO.KeysDown[512] array -enum ImGuiKey_ -{ - ImGuiKey_Tab, - ImGuiKey_LeftArrow, - ImGuiKey_RightArrow, - ImGuiKey_UpArrow, - ImGuiKey_DownArrow, - ImGuiKey_PageUp, - ImGuiKey_PageDown, - ImGuiKey_Home, - ImGuiKey_End, - ImGuiKey_Insert, - ImGuiKey_Delete, - ImGuiKey_Backspace, - ImGuiKey_Space, - ImGuiKey_Enter, - ImGuiKey_Escape, - ImGuiKey_A, // for text edit CTRL+A: select all - ImGuiKey_C, // for text edit CTRL+C: copy - ImGuiKey_V, // for text edit CTRL+V: paste - ImGuiKey_X, // for text edit CTRL+X: cut - ImGuiKey_Y, // for text edit CTRL+Y: redo - ImGuiKey_Z, // for text edit CTRL+Z: undo - ImGuiKey_COUNT -}; - -// [BETA] Gamepad/Keyboard directional navigation -// Keyboard: Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. -// Gamepad: Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). -// Read instructions in imgui.cpp for more details. -enum ImGuiNavInput_ -{ - // Gamepad Mapping - ImGuiNavInput_Activate, // activate / open / toggle / tweak value // e.g. Circle (PS4), A (Xbox), B (Switch), Space (Keyboard) - ImGuiNavInput_Cancel, // cancel / close / exit // e.g. Cross (PS4), B (Xbox), A (Switch), Escape (Keyboard) - ImGuiNavInput_Input, // text input / on-screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard) - ImGuiNavInput_Menu, // tap: toggle menu / hold: focus, move, resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard) - ImGuiNavInput_DpadLeft, // move / tweak / resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard) - ImGuiNavInput_DpadRight, // - ImGuiNavInput_DpadUp, // - ImGuiNavInput_DpadDown, // - ImGuiNavInput_LStickLeft, // scroll / move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down - ImGuiNavInput_LStickRight, // - ImGuiNavInput_LStickUp, // - ImGuiNavInput_LStickDown, // - ImGuiNavInput_FocusPrev, // next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) - ImGuiNavInput_FocusNext, // prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) - ImGuiNavInput_TweakSlow, // slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) - ImGuiNavInput_TweakFast, // faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) - - // [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them. - // Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) may be directly reading from io.KeyDown[] instead of io.NavInputs[]. - ImGuiNavInput_KeyMenu_, // toggle menu // = io.KeyAlt - ImGuiNavInput_KeyLeft_, // move left // = Arrow keys - ImGuiNavInput_KeyRight_, // move right - ImGuiNavInput_KeyUp_, // move up - ImGuiNavInput_KeyDown_, // move down - ImGuiNavInput_COUNT, - ImGuiNavInput_InternalStart_ = ImGuiNavInput_KeyMenu_ -}; - -// [BETA] Gamepad/Keyboard directional navigation flags, stored in io.NavFlags -enum ImGuiNavFlags_ -{ - ImGuiNavFlags_EnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. NewFrame() will automatically fill io.NavInputs[] based on io.KeyDown[]. - ImGuiNavFlags_EnableGamepad = 1 << 1, // Master gamepad navigation enable flag. This is mostly to instruct your imgui back-end to fill io.NavInputs[]. - ImGuiNavFlags_MoveMouse = 1 << 2, // Request navigation to allow moving the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantMoveMouse=true. If enabled you MUST honor io.WantMoveMouse requests in your binding, otherwise ImGui will react as if the mouse is jumping around back and forth. - ImGuiNavFlags_NoCaptureKeyboard = 1 << 3 // Do not set the io.WantCaptureKeyboard flag with io.NavActive is set. -}; + ImGuiDragDropFlags_None = 0, + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // Disable preview tooltip. By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disables this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disables this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + ImGuiDragDropFlags_PayloadAutoExpire = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + ImGuiDragDropFlags_PayloadNoCrossContext = 1 << 6, // Hint to specify that the payload may not be copied outside current dear imgui context. + ImGuiDragDropFlags_PayloadNoCrossProcess = 1 << 7, // Hint to specify that the payload may not be copied outside current process. + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect, // For peeking ahead and inspecting the payload before delivery. -// Enumeration for PushStyleColor() / PopStyleColor() -enum ImGuiCol_ -{ - ImGuiCol_Text, - ImGuiCol_TextDisabled, - ImGuiCol_WindowBg, // Background of normal windows - ImGuiCol_ChildBg, // Background of child windows - ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows - ImGuiCol_Border, - ImGuiCol_BorderShadow, - ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input - ImGuiCol_FrameBgHovered, - ImGuiCol_FrameBgActive, - ImGuiCol_TitleBg, - ImGuiCol_TitleBgActive, - ImGuiCol_TitleBgCollapsed, - ImGuiCol_MenuBarBg, - ImGuiCol_ScrollbarBg, - ImGuiCol_ScrollbarGrab, - ImGuiCol_ScrollbarGrabHovered, - ImGuiCol_ScrollbarGrabActive, - ImGuiCol_CheckMark, - ImGuiCol_SliderGrab, - ImGuiCol_SliderGrabActive, - ImGuiCol_Button, - ImGuiCol_ButtonHovered, - ImGuiCol_ButtonActive, - ImGuiCol_Header, - ImGuiCol_HeaderHovered, - ImGuiCol_HeaderActive, - ImGuiCol_Separator, - ImGuiCol_SeparatorHovered, - ImGuiCol_SeparatorActive, - ImGuiCol_ResizeGrip, - ImGuiCol_ResizeGripHovered, - ImGuiCol_ResizeGripActive, - ImGuiCol_CloseButton, - ImGuiCol_CloseButtonHovered, - ImGuiCol_CloseButtonActive, - ImGuiCol_PlotLines, - ImGuiCol_PlotLinesHovered, - ImGuiCol_PlotHistogram, - ImGuiCol_PlotHistogramHovered, - ImGuiCol_TextSelectedBg, - ImGuiCol_ModalWindowDarkening, // darken entire screen when a modal window is active - ImGuiCol_DragDropTarget, - ImGuiCol_NavHighlight, // gamepad/keyboard: current highlighted item - ImGuiCol_NavWindowingHighlight, // gamepad/keyboard: when holding NavMenu to focus/move/resize windows - ImGuiCol_COUNT - -// Obsolete names (will be removed) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - //, ImGuiCol_ComboBg = ImGuiCol_PopupBg // ComboBg has been merged with PopupBg, so a redirect isn't accurate. - , - ImGuiCol_ChildWindowBg = ImGuiCol_ChildBg, - ImGuiCol_Column = ImGuiCol_Separator, - ImGuiCol_ColumnHovered = ImGuiCol_SeparatorHovered, - ImGuiCol_ColumnActive = ImGuiCol_SeparatorActive + ImGuiDragDropFlags_SourceAutoExpirePayload = ImGuiDragDropFlags_PayloadAutoExpire, // Renamed in 1.90.9 #endif }; -// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. -// NB: the enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. During initialization, feel free to just poke into ImGuiStyle directly. -// NB: if changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. -enum ImGuiStyleVar_ +// Standard Drag and Drop payload types. You can define you own payload types using short strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3]: Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4]: Standard type for colors. User code may use this type. + +// A primary data type +enum ImGuiDataType_ { - // Enum name ......................// Member in ImGuiStyle structure (see ImGuiStyle for descriptions) - ImGuiStyleVar_Alpha, // float Alpha - ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding - ImGuiStyleVar_WindowRounding, // float WindowRounding - ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize - ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize - ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign - ImGuiStyleVar_ChildRounding, // float ChildRounding - ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize - ImGuiStyleVar_PopupRounding, // float PopupRounding - ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize - ImGuiStyleVar_FramePadding, // ImVec2 FramePadding - ImGuiStyleVar_FrameRounding, // float FrameRounding - ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize - ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing - ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing - ImGuiStyleVar_IndentSpacing, // float IndentSpacing - ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize - ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding - ImGuiStyleVar_GrabMinSize, // float GrabMinSize - ImGuiStyleVar_GrabRounding, // float GrabRounding - ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign - ImGuiStyleVar_Count_ - -// Obsolete names (will be removed) -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , - ImGuiStyleVar_ChildWindowRounding = ImGuiStyleVar_ChildRounding -#endif + ImGuiDataType_S8, // signed char / char (with sensible compilers) + ImGuiDataType_U8, // unsigned char + ImGuiDataType_S16, // short + ImGuiDataType_U16, // unsigned short + ImGuiDataType_S32, // int + ImGuiDataType_U32, // unsigned int + ImGuiDataType_S64, // long long / __int64 + ImGuiDataType_U64, // unsigned long long / unsigned __int64 + ImGuiDataType_Float, // float + ImGuiDataType_Double, // double + ImGuiDataType_Bool, // bool (provided for user convenience, not supported by scalar widgets) + ImGuiDataType_COUNT }; -// Enumeration for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() -enum ImGuiColorEditFlags_ +// A cardinal direction +enum ImGuiDir : int { - ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (read 3 components from the input pointer). - ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on colored square. - ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. - ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable colored square preview next to the inputs. (e.g. to show only the inputs) - ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview colored square). - ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. - ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). - ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small colored square preview instead. - // User Options (right-click on widget to change some of them). You can set application defaults using SetColorEditOptions(). The idea is that you probably don't want to override them in most of your calls, let the user choose and/or call SetColorEditOptions() during startup. - ImGuiColorEditFlags_AlphaBar = 1 << 9, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. - ImGuiColorEditFlags_AlphaPreview = 1 << 10, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. - ImGuiColorEditFlags_AlphaPreviewHalf = 1 << 11, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. - ImGuiColorEditFlags_HDR = 1 << 12, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). - ImGuiColorEditFlags_RGB = 1 << 13, // [Inputs] // ColorEdit: choose one among RGB/HSV/HEX. ColorPicker: choose any combination using RGB/HSV/HEX. - ImGuiColorEditFlags_HSV = 1 << 14, // [Inputs] // " - ImGuiColorEditFlags_HEX = 1 << 15, // [Inputs] // " - ImGuiColorEditFlags_Uint8 = 1 << 16, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. - ImGuiColorEditFlags_Float = 1 << 17, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. - ImGuiColorEditFlags_PickerHueBar = 1 << 18, // [PickerMode] // ColorPicker: bar for Hue, rectangle for Sat/Value. - ImGuiColorEditFlags_PickerHueWheel = 1 << 19, // [PickerMode] // ColorPicker: wheel for Hue, triangle for Sat/Value. - // Internals/Masks - ImGuiColorEditFlags__InputsMask = ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV | ImGuiColorEditFlags_HEX, - ImGuiColorEditFlags__DataTypeMask = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, - ImGuiColorEditFlags__PickerMask = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags__OptionsDefault = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_PickerHueBar // Change application default using SetColorEditOptions() + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_COUNT }; -// Enumeration for GetMouseCursor() -enum ImGuiMouseCursor_ +// A sorting direction +enum ImGuiSortDirection : ImU8 { - ImGuiMouseCursor_None = -1, - ImGuiMouseCursor_Arrow = 0, - ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. - ImGuiMouseCursor_ResizeAll, // Unused - ImGuiMouseCursor_ResizeNS, // When hovering over an horizontal border - ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column - ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window - ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window - ImGuiMouseCursor_Count_ + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. }; -// Condition for ImGui::SetWindow***(), SetNextWindow***(), SetNextTreeNode***() functions -// All those functions treat 0 as a shortcut to ImGuiCond_Always. From the point of view of the user use this as an enum (don't combine multiple values into flags). -enum ImGuiCond_ +// Since 1.90, defining IMGUI_DISABLE_OBSOLETE_FUNCTIONS automatically defines IMGUI_DISABLE_OBSOLETE_KEYIO as well. +#if defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && !defined(IMGUI_DISABLE_OBSOLETE_KEYIO) +#define IMGUI_DISABLE_OBSOLETE_KEYIO +#endif + +// A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value): can represent Keyboard, Mouse and Gamepad values. +// All our named keys are >= 512. Keys value 0 to 511 are left unused as legacy native/opaque key values (< 1.87). +// Since >= 1.89 we increased typing (went from int to enum), some legacy code may need a cast to ImGuiKey. +// Read details about the 1.87 and 1.89 transition : https://github.com/ocornut/imgui/issues/4921 +// Note that "Keys" related to physical keys and are not the same concept as input "Characters", the later are submitted via io.AddInputCharacter(). +// The keyboard key enum values are named after the keys on a standard US keyboard, and on other keyboard types the keys reported may not match the keycaps. +enum ImGuiKey : int { - ImGuiCond_Always = 1 << 0, // Set the variable - ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call with succeed) - ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the window has no saved data (if doesn't exist in the .ini file) - ImGuiCond_Appearing = 1 << 3 // Set the variable if the window is appearing after being hidden/inactive (or the first time) + // Keyboard + ImGuiKey_None = 0, + ImGuiKey_Tab = 512, // == ImGuiKey_NamedKey_BEGIN + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper, + ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper, + ImGuiKey_Menu, + ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, + ImGuiKey_A, ImGuiKey_B, ImGuiKey_C, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_I, ImGuiKey_J, + ImGuiKey_K, ImGuiKey_L, ImGuiKey_M, ImGuiKey_N, ImGuiKey_O, ImGuiKey_P, ImGuiKey_Q, ImGuiKey_R, ImGuiKey_S, ImGuiKey_T, + ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, + ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, + ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_F13, ImGuiKey_F14, ImGuiKey_F15, ImGuiKey_F16, ImGuiKey_F17, ImGuiKey_F18, + ImGuiKey_F19, ImGuiKey_F20, ImGuiKey_F21, ImGuiKey_F22, ImGuiKey_F23, ImGuiKey_F24, + ImGuiKey_Apostrophe, // ' + ImGuiKey_Comma, // , + ImGuiKey_Minus, // - + ImGuiKey_Period, // . + ImGuiKey_Slash, // / + ImGuiKey_Semicolon, // ; + ImGuiKey_Equal, // = + ImGuiKey_LeftBracket, // [ + ImGuiKey_Backslash, // \ (this text inhibit multiline comment caused by backslash) + ImGuiKey_RightBracket, // ] + ImGuiKey_GraveAccent, // ` + ImGuiKey_CapsLock, + ImGuiKey_ScrollLock, + ImGuiKey_NumLock, + ImGuiKey_PrintScreen, + ImGuiKey_Pause, + ImGuiKey_Keypad0, ImGuiKey_Keypad1, ImGuiKey_Keypad2, ImGuiKey_Keypad3, ImGuiKey_Keypad4, + ImGuiKey_Keypad5, ImGuiKey_Keypad6, ImGuiKey_Keypad7, ImGuiKey_Keypad8, ImGuiKey_Keypad9, + ImGuiKey_KeypadDecimal, + ImGuiKey_KeypadDivide, + ImGuiKey_KeypadMultiply, + ImGuiKey_KeypadSubtract, + ImGuiKey_KeypadAdd, + ImGuiKey_KeypadEnter, + ImGuiKey_KeypadEqual, + ImGuiKey_AppBack, // Available on some keyboard/mouses. Often referred as "Browser Back" + ImGuiKey_AppForward, + + // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION ACTION + // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets) + ImGuiKey_GamepadStart, // Menu (Xbox) + (Switch) Start/Options (PS) + ImGuiKey_GamepadBack, // View (Xbox) - (Switch) Share (PS) + ImGuiKey_GamepadFaceLeft, // X (Xbox) Y (Switch) Square (PS) // Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows) + ImGuiKey_GamepadFaceRight, // B (Xbox) A (Switch) Circle (PS) // Cancel / Close / Exit + ImGuiKey_GamepadFaceUp, // Y (Xbox) X (Switch) Triangle (PS) // Text Input / On-screen Keyboard + ImGuiKey_GamepadFaceDown, // A (Xbox) B (Switch) Cross (PS) // Activate / Open / Toggle / Tweak + ImGuiKey_GamepadDpadLeft, // D-pad Left // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadRight, // D-pad Right // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadUp, // D-pad Up // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadDown, // D-pad Down // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadL1, // L Bumper (Xbox) L (Switch) L1 (PS) // Tweak Slower / Focus Previous (in Windowing mode) + ImGuiKey_GamepadR1, // R Bumper (Xbox) R (Switch) R1 (PS) // Tweak Faster / Focus Next (in Windowing mode) + ImGuiKey_GamepadL2, // L Trig. (Xbox) ZL (Switch) L2 (PS) [Analog] + ImGuiKey_GamepadR2, // R Trig. (Xbox) ZR (Switch) R2 (PS) [Analog] + ImGuiKey_GamepadL3, // L Stick (Xbox) L3 (Switch) L3 (PS) + ImGuiKey_GamepadR3, // R Stick (Xbox) R3 (Switch) R3 (PS) + ImGuiKey_GamepadLStickLeft, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickRight, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickUp, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickDown, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadRStickLeft, // [Analog] + ImGuiKey_GamepadRStickRight, // [Analog] + ImGuiKey_GamepadRStickUp, // [Analog] + ImGuiKey_GamepadRStickDown, // [Analog] + + // Aliases: Mouse Buttons (auto-submitted from AddMouseButtonEvent() calls) + // - This is mirroring the data also written to io.MouseDown[], io.MouseWheel, in a format allowing them to be accessed via standard key API. + ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle, ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY, + + // [Internal] Reserved for mod storage + ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt, ImGuiKey_ReservedForModSuper, + ImGuiKey_COUNT, + + // Keyboard Modifiers (explicitly submitted by backend via AddKeyEvent() calls) + // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing + // them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc. + // - Code polling every key (e.g. an interface to detect a key press for input mapping) might want to ignore those + // and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiMod_Ctrl). + // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys. + // In practice: it's complicated; mods are often provided from different sources. Keyboard layout, IME, sticky keys and + // backends tend to interfere and break that equivalence. The safer decision is to relay that ambiguity down to the end-user... + // - On macOS, we swap Cmd(Super) and Ctrl keys at the time of the io.AddKeyEvent() call. + ImGuiMod_None = 0, + ImGuiMod_Ctrl = 1 << 12, // Ctrl (non-macOS), Cmd (macOS) + ImGuiMod_Shift = 1 << 13, // Shift + ImGuiMod_Alt = 1 << 14, // Option/Menu + ImGuiMod_Super = 1 << 15, // Windows/Super (non-macOS), Ctrl (macOS) + ImGuiMod_Mask_ = 0xF000, // 4-bits + + // [Internal] Prior to 1.87 we required user to fill io.KeysDown[512] using their own native index + the io.KeyMap[] array. + // We are ditching this method but keeping a legacy path for user code doing e.g. IsKeyPressed(MY_NATIVE_KEY_CODE) + // If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END. + ImGuiKey_NamedKey_BEGIN = 512, + ImGuiKey_NamedKey_END = ImGuiKey_COUNT, + ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, // Size of KeysData[]: only hold named keys + ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, // Accesses to io.KeysData[] must use (key - ImGuiKey_KeysData_OFFSET) index. +#else + ImGuiKey_KeysData_SIZE = ImGuiKey_COUNT, // Size of KeysData[]: hold legacy 0..512 keycodes + named keys + ImGuiKey_KeysData_OFFSET = 0, // Accesses to io.KeysData[] must use (key - ImGuiKey_KeysData_OFFSET) index. +#endif -// Obsolete names (will be removed) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , - ImGuiSetCond_Always = ImGuiCond_Always, - ImGuiSetCond_Once = ImGuiCond_Once, - ImGuiSetCond_FirstUseEver = ImGuiCond_FirstUseEver, - ImGuiSetCond_Appearing = ImGuiCond_Appearing + ImGuiMod_Shortcut = ImGuiMod_Ctrl, // Removed in 1.90.7, you can now simply use ImGuiMod_Ctrl + ImGuiKey_ModCtrl = ImGuiMod_Ctrl, ImGuiKey_ModShift = ImGuiMod_Shift, ImGuiKey_ModAlt = ImGuiMod_Alt, ImGuiKey_ModSuper = ImGuiMod_Super, // Renamed in 1.89 + //ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 #endif }; -// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). -// During the frame, prefer using ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. -struct ImGuiStyle +// Flags for Shortcut(), SetNextItemShortcut(), +// (and for upcoming extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() that are still in imgui_internal.h) +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlags_ { - float Alpha; // Global alpha applies to everything in ImGui. - ImVec2 WindowPadding; // Padding within a window. - float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. - float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). - ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constraint individual windows, use SetNextWindowSizeConstraints(). - ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. - float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. - float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). - float PopupRounding; // Radius of popup window corners rounding. - float PopupBorderSize; // Thickness of border around popup windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). - ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). - float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). - float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). - ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. - ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). - ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! - float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). - float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. - float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. - float ScrollbarRounding; // Radius of grab corners for scrollbar. - float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. - float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. - ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f,0.5f) for horizontally+vertically centered. - ImVec2 DisplayWindowPadding; // Window positions are clamped to be visible within the display area by at least this amount. Only covers regular windows. - ImVec2 DisplaySafeAreaPadding; // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. - float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. - bool AntiAliasedLines; // Enable anti-aliasing on lines/borders. Disable if you are really tight on CPU/GPU. - bool AntiAliasedFill; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) - float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. - ImVec4 Colors[ImGuiCol_COUNT]; - - IMGUI_API ImGuiStyle(); - IMGUI_API void ScaleAllSizes(float scale_factor); -}; - -// This is where your app communicate with ImGui. Access via ImGui::GetIO(). -// Read 'Programmer guide' section in .cpp file for general usage. -struct ImGuiIO -{ - //------------------------------------------------------------------ - // Settings (fill once) // Default value: - //------------------------------------------------------------------ - - ImVec2 DisplaySize; // // Display size, in pixels. For clamping windows positions. - float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. - ImGuiNavFlags NavFlags; // = 0x00 // See ImGuiNavFlags_. Gamepad/keyboard navigation options. - float IniSavingRate; // = 5.0f // Maximum time between saving positions/sizes to .ini file, in seconds. - const char *IniFilename; // = "imgui.ini" // Path to .ini file. NULL to disable .ini saving. - const char *LogFilename; // = "imgui_log.txt" // Path to .log file (default parameter to ImGui::LogToFile when no file is specified). - float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. - float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. - float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. - int KeyMap[ImGuiKey_COUNT]; // // Map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. - float KeyRepeatDelay; // = 0.250f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). - float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. - void *UserData; // = NULL // Store your own data for retrieval by callbacks. - - ImFontAtlas *Fonts; // // Load and assemble one or more fonts into a single tightly packed texture. Output to Fonts array. - float FontGlobalScale; // = 1.0f // Global scale all fonts - bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. - ImFont *FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. - ImVec2 DisplayFramebufferScale; // = (1.0f,1.0f) // For retina display or other situations where window coordinates are different from framebuffer coordinates. User storage only, presently not used by ImGui. - ImVec2 DisplayVisibleMin; // (0.0f,0.0f) // If you use DisplaySize as a virtual space larger than your screen, set DisplayVisibleMin/Max to the visible area. - ImVec2 DisplayVisibleMax; // (0.0f,0.0f) // If the values are the same, we defaults to Min=(0.0f) and Max=DisplaySize - - // Advanced/subtle behaviors - bool OptMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl - bool OptCursorBlink; // = true // Enable blinking cursor, for users who consider it annoying. - - //------------------------------------------------------------------ - // Settings (User Functions) - //------------------------------------------------------------------ - - // Optional: access OS clipboard - // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) - const char *(*GetClipboardTextFn)(void *user_data); - void (*SetClipboardTextFn)(void *user_data, const char *text); - void *ClipboardUserData; - - // Optional: notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME in Windows) - // (default to use native imm32 api on Windows) - void (*ImeSetInputScreenPosFn)(int x, int y); - void *ImeWindowHandle; // (Windows) Set this to your HWND to get automatic IME cursor positioning. + ImGuiInputFlags_None = 0, + ImGuiInputFlags_Repeat = 1 << 0, // Enable repeat. Return true on successive repeats. Default for legacy IsKeyPressed(). NOT Default for legacy IsMouseClicked(). MUST BE == 1. + + // Flags for Shortcut(), SetNextItemShortcut() + // - Routing policies: RouteGlobal+OverActive >> RouteActive or RouteFocused (if owner is active item) >> RouteGlobal+OverFocused >> RouteFocused (if in focused window stack) >> RouteGlobal. + // - Default policy is RouteFocused. Can select only 1 policy among all available. + ImGuiInputFlags_RouteActive = 1 << 10, // Route to active item only. + ImGuiInputFlags_RouteFocused = 1 << 11, // Route to windows in the focus stack (DEFAULT). Deep-most focused window takes inputs. Active item takes inputs over deep-most focused window. + ImGuiInputFlags_RouteGlobal = 1 << 12, // Global route (unless a focused window or active item registered the route). + ImGuiInputFlags_RouteAlways = 1 << 13, // Do not register route, poll keys directly. + // - Routing options + ImGuiInputFlags_RouteOverFocused = 1 << 14, // Option: global route: higher priority than focused route (unless active item in focused route). + ImGuiInputFlags_RouteOverActive = 1 << 15, // Option: global route: higher priority than active item. Unlikely you need to use that: will interfere with every active items, e.g. CTRL+A registered by InputText will be overridden by this. May not be fully honored as user/internal code is likely to always assume they can access keys when active. + ImGuiInputFlags_RouteUnlessBgFocused = 1 << 16, // Option: global route: will not be applied if underlying background/void is focused (== no Dear ImGui windows are focused). Useful for overlay applications. + ImGuiInputFlags_RouteFromRootWindow = 1 << 17, // Option: route evaluated from the point of view of root window rather than current window. + + // Flags for SetNextItemShortcut() + ImGuiInputFlags_Tooltip = 1 << 18, // Automatically display a tooltip when hovering item [BETA] Unsure of right api (opt-in/opt-out) +}; -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - // [OBSOLETE] Rendering function, will be automatically called in Render(). Please call your rendering function yourself now! You can obtain the ImDrawData* by calling ImGui::GetDrawData() after Render(). - // See example applications if you are unsure of how to implement this. - void (*RenderDrawListsFn)(ImDrawData *data); +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO +// OBSOLETED in 1.88 (from July 2022): ImGuiNavInput and io.NavInputs[]. +// Official backends between 1.60 and 1.86: will keep working and feed gamepad inputs as long as IMGUI_DISABLE_OBSOLETE_KEYIO is not set. +// Custom backends: feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. +enum ImGuiNavInput +{ + ImGuiNavInput_Activate, ImGuiNavInput_Cancel, ImGuiNavInput_Input, ImGuiNavInput_Menu, ImGuiNavInput_DpadLeft, ImGuiNavInput_DpadRight, ImGuiNavInput_DpadUp, ImGuiNavInput_DpadDown, + ImGuiNavInput_LStickLeft, ImGuiNavInput_LStickRight, ImGuiNavInput_LStickUp, ImGuiNavInput_LStickDown, ImGuiNavInput_FocusPrev, ImGuiNavInput_FocusNext, ImGuiNavInput_TweakSlow, ImGuiNavInput_TweakFast, + ImGuiNavInput_COUNT, +}; #endif - //------------------------------------------------------------------ - // Input - Fill before calling NewFrame() - //------------------------------------------------------------------ - - ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX,-FLT_MAX) if mouse is unavailable (on another screen, etc.) - bool MouseDown[5]; // Mouse buttons: left, right, middle + extras. ImGui itself mostly only uses left button (BeginPopupContext** are using right button). Others buttons allows us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. - float MouseWheel; // Mouse wheel: 1 unit scrolls about 5 lines text. - float MouseWheelH; // Mouse wheel (Horizontal). Most users don't have a mouse with an horizontal wheel, may not be filled by all back-ends. - bool MouseDrawCursor; // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). - bool KeyCtrl; // Keyboard modifier pressed: Control - bool KeyShift; // Keyboard modifier pressed: Shift - bool KeyAlt; // Keyboard modifier pressed: Alt - bool KeySuper; // Keyboard modifier pressed: Cmd/Super/Windows - bool KeysDown[512]; // Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). - ImWchar InputCharacters[16 + 1]; // List of characters input (translated by user from keypress+keyboard state). Fill using AddInputCharacter() helper. - float NavInputs[ImGuiNavInput_COUNT]; // Gamepad inputs (keyboard keys will be auto-mapped and be written here by ImGui::NewFrame) - - // Functions - IMGUI_API void AddInputCharacter(ImWchar c); // Add new character into InputCharacters[] - IMGUI_API void AddInputCharactersUTF8(const char *utf8_chars); // Add new characters into InputCharacters[] from an UTF-8 string - inline void ClearInputCharacters() - { - InputCharacters[0] = 0; - } // Clear the text input buffer manually - - //------------------------------------------------------------------ - // Output - Retrieve after calling NewFrame() - //------------------------------------------------------------------ - - bool WantCaptureMouse; // When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application. This is set by ImGui when it wants to use your mouse (e.g. unclicked mouse is hovering a window, or a widget is active). - bool WantCaptureKeyboard; // When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application. This is set by ImGui when it wants to use your keyboard inputs. - bool WantTextInput; // Mobile/console: when io.WantTextInput is true, you may display an on-screen keyboard. This is set by ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). - bool WantMoveMouse; // MousePos has been altered, back-end should reposition mouse on next frame. Set only when ImGuiNavFlags_MoveMouse flag is enabled in io.NavFlags. - bool NavActive; // Directional navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. - bool NavVisible; // Directional navigation is visible and allowed (will handle ImGuiKey_NavXXX events). - float Framerate; // Application framerate estimation, in frame per second. Solely for convenience. Rolling average estimation based on IO.DeltaTime over 120 frames - int MetricsRenderVertices; // Vertices output during last call to Render() - int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 - int MetricsActiveWindows; // Number of visible root windows (exclude child windows) - ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. - - //------------------------------------------------------------------ - // [Internal] ImGui will maintain those fields. Forward compatibility not guaranteed! - //------------------------------------------------------------------ - - ImVec2 MousePosPrev; // Previous mouse position temporary storage (nb: not for public use, set to MousePos in NewFrame()) - ImVec2 MouseClickedPos[5]; // Position at time of clicking - float MouseClickedTime[5]; // Time of last click (used to figure out double-click) - bool MouseClicked[5]; // Mouse button went from !Down to Down - bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? - bool MouseReleased[5]; // Mouse button went from Down to !Down - bool MouseDownOwned[5]; // Track if button was clicked inside a window. We don't request mouse capture from the application if click started outside ImGui bounds. - float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) - float MouseDownDurationPrev[5]; // Previous time the mouse button has been down - ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point - float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point - float KeysDownDuration[512]; // Duration the keyboard key has been down (0.0f == just pressed) - float KeysDownDurationPrev[512]; // Previous duration the key has been down - float NavInputsDownDuration[ImGuiNavInput_COUNT]; - float NavInputsDownDurationPrev[ImGuiNavInput_COUNT]; - - IMGUI_API ImGuiIO(); +// Configuration flags stored in io.ConfigFlags. Set by user/application. +enum ImGuiConfigFlags_ +{ + ImGuiConfigFlags_None = 0, + ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. Enable full Tabbing + directional arrows + space/enter to activate. + ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. Backend also needs to set ImGuiBackendFlags_HasGamepad. + ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantSetMousePos=true. If enabled you MUST honor io.WantSetMousePos requests in your backend, otherwise ImGui will react as if the mouse is jumping around back and forth. + ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // Instruct navigation to not set the io.WantCaptureKeyboard flag when io.NavActive is set. + ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct dear imgui to disable mouse inputs and interactions. + ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + ImGuiConfigFlags_NoKeyboard = 1 << 6, // Instruct dear imgui to disable keyboard inputs and interactions. This is done by ignoring keyboard events and clearing existing states. + + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are NOT used by core Dear ImGui) + ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. + ImGuiConfigFlags_IsTouchScreen = 1 << 21, // Application is using a touch screen instead of a mouse. }; -//----------------------------------------------------------------------------- -// Obsolete functions (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) -//----------------------------------------------------------------------------- +// Backend capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom backend. +enum ImGuiBackendFlags_ +{ + ImGuiBackendFlags_None = 0, + ImGuiBackendFlags_HasGamepad = 1 << 0, // Backend Platform supports gamepad and currently has one connected. + ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. + ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if ImGuiConfigFlags_NavEnableSetMousePos is set). + ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, // Title bar + ImGuiCol_TitleBgActive, // Title bar when focused + ImGuiCol_TitleBgCollapsed, // Title bar when collapsed + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, // Header* colors are used for CollapsingHeader, TreeNode, Selectable, MenuItem + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, // Resize grip in lower-right and lower-left corners of windows. + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_TabHovered, // Tab background, when hovered + ImGuiCol_Tab, // Tab background, when tab-bar is focused & tab is unselected + ImGuiCol_TabSelected, // Tab background, when tab-bar is focused & tab is selected + ImGuiCol_TabSelectedOverline, // Tab horizontal overline, when tab-bar is focused & tab is selected + ImGuiCol_TabDimmed, // Tab background, when tab-bar is unfocused & tab is unselected + ImGuiCol_TabDimmedSelected, // Tab background, when tab-bar is unfocused & tab is selected + ImGuiCol_TabDimmedSelectedOverline,//..horizontal overline, when tab-bar is unfocused & tab is selected + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableBorderStrong, // Table outer and header borders (prefer using Alpha=1.0 here) + ImGuiCol_TableBorderLight, // Table inner borders (prefer using Alpha=1.0 here) + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) + ImGuiCol_TextLink, // Hyperlink color + ImGuiCol_TextSelectedBg, + ImGuiCol_DragDropTarget, // Rectangle highlighting a drop target + ImGuiCol_NavHighlight, // Gamepad/keyboard: current highlighted item + ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB + ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the CTRL+TAB window list, when active + ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active + ImGuiCol_COUNT, #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -namespace ImGui + ImGuiCol_TabActive = ImGuiCol_TabSelected, // [renamed in 1.90.9] + ImGuiCol_TabUnfocused = ImGuiCol_TabDimmed, // [renamed in 1.90.9] + ImGuiCol_TabUnfocusedActive = ImGuiCol_TabDimmedSelected, // [renamed in 1.90.9] +#endif +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. +// During initialization or between frames, feel free to just poke into ImGuiStyle directly. +// - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +// - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ { -// OBSOLETED in 1.60 (from Dec 2017) -static inline bool IsAnyWindowFocused() + // Enum name -------------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_TabBorderSize, // float TabBorderSize + ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize + ImGuiStyleVar_TabBarOverlineSize, // float TabBarOverlineSize + ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle + ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize + ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign + ImGuiStyleVar_SeparatorTextPadding, // ImVec2 SeparatorTextPadding + ImGuiStyleVar_COUNT +}; + +// Flags for InvisibleButton() [extended in imgui_internal.h] +enum ImGuiButtonFlags_ { - return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); -} -static inline bool IsAnyWindowHovered() + ImGuiButtonFlags_None = 0, + ImGuiButtonFlags_MouseButtonLeft = 1 << 0, // React on left mouse button (default) + ImGuiButtonFlags_MouseButtonRight = 1 << 1, // React on right mouse button + ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, // React on center mouse button + ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle, // [Internal] + //ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft, +}; + +// Flags for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() +enum ImGuiColorEditFlags_ { - return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); -} -static inline ImVec2 CalcItemRectClosestPoint(const ImVec2 &pos, bool on_edge = false, float outward = 0.f) + ImGuiColorEditFlags_None = 0, + ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (will only read 3 components from the input pointer). + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on color square. + ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable color square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview color square). + ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. + ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small color square preview instead. + ImGuiColorEditFlags_NoDragDrop = 1 << 9, // // ColorEdit: disable drag and drop target. ColorButton: disable drag and drop source. + ImGuiColorEditFlags_NoBorder = 1 << 10, // // ColorButton: disable border (which is enforced by default) + + // User Options (right-click on widget to change some of them). + ImGuiColorEditFlags_AlphaBar = 1 << 16, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. + ImGuiColorEditFlags_AlphaPreview = 1 << 17, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 18, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. + ImGuiColorEditFlags_HDR = 1 << 19, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). + ImGuiColorEditFlags_DisplayRGB = 1 << 20, // [Display] // ColorEdit: override _display_ type among RGB/HSV/Hex. ColorPicker: select any combination using one or more of RGB/HSV/Hex. + ImGuiColorEditFlags_DisplayHSV = 1 << 21, // [Display] // " + ImGuiColorEditFlags_DisplayHex = 1 << 22, // [Display] // " + ImGuiColorEditFlags_Uint8 = 1 << 23, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. + ImGuiColorEditFlags_Float = 1 << 24, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. + ImGuiColorEditFlags_PickerHueBar = 1 << 25, // [Picker] // ColorPicker: bar for Hue, rectangle for Sat/Value. + ImGuiColorEditFlags_PickerHueWheel = 1 << 26, // [Picker] // ColorPicker: wheel for Hue, triangle for Sat/Value. + ImGuiColorEditFlags_InputRGB = 1 << 27, // [Input] // ColorEdit, ColorPicker: input and output data in RGB format. + ImGuiColorEditFlags_InputHSV = 1 << 28, // [Input] // ColorEdit, ColorPicker: input and output data in HSV format. + + // Defaults Options. You can set application defaults using SetColorEditOptions(). The intent is that you probably don't want to + // override them in most of your calls. Let the user choose via the option menu and/or call SetColorEditOptions() once during startup. + ImGuiColorEditFlags_DefaultOptions_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, + + // [Internal] Masks + ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_DisplayHex, + ImGuiColorEditFlags_DataTypeMask_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, + ImGuiColorEditFlags_PickerMask_ = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_InputMask_ = ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, + + // Obsolete names + //ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] +}; + +// Flags for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +// We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigDragClickToInputText) +enum ImGuiSliderFlags_ { - (void) on_edge; - (void) outward; - IM_ASSERT(0); - return pos; -} -// OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) -static inline void ShowTestWindow() + ImGuiSliderFlags_None = 0, + ImGuiSliderFlags_AlwaysClamp = 1 << 4, // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. + ImGuiSliderFlags_Logarithmic = 1 << 5, // Make the widget logarithmic (linear otherwise). Consider using ImGuiSliderFlags_NoRoundToFormat with this if using a format-string with small amount of digits. + ImGuiSliderFlags_NoRoundToFormat = 1 << 6, // Disable rounding underlying value to match precision of the display format string (e.g. %.3f values are rounded to those 3 digits). + ImGuiSliderFlags_NoInput = 1 << 7, // Disable CTRL+Click or Enter key allowing to input text directly into the widget. + ImGuiSliderFlags_WrapAround = 1 << 8, // Enable wrapping around from max to min and from min to max (only supported by DragXXX() functions for now. + ImGuiSliderFlags_InvalidMask_ = 0x7000000F, // [Internal] We treat using those bits as being potentially a 'float power' argument from the previous API that has got miscast to this enum, and will trigger an assert if needed. + + // Obsolete names + //ImGuiSliderFlags_ClampOnInput = ImGuiSliderFlags_AlwaysClamp, // [renamed in 1.79] +}; + +// Identify a mouse button. +// Those values are guaranteed to be stable and we frequently use 0/1 directly. Named enums provided for convenience. +enum ImGuiMouseButton_ { - return ShowDemoWindow(); -} -static inline bool IsRootWindowFocused() + ImGuiMouseButton_Left = 0, + ImGuiMouseButton_Right = 1, + ImGuiMouseButton_Middle = 2, + ImGuiMouseButton_COUNT = 5 +}; + +// Enumeration for GetMouseCursor() +// User code may request backend to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here +enum ImGuiMouseCursor_ { - return IsWindowFocused(ImGuiFocusedFlags_RootWindow); -} -static inline bool IsRootWindowOrAnyChildFocused() + ImGuiMouseCursor_None = -1, + ImGuiMouseCursor_Arrow = 0, + ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. + ImGuiMouseCursor_ResizeAll, // (Unused by Dear ImGui functions) + ImGuiMouseCursor_ResizeNS, // When hovering over a horizontal border + ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column + ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window + ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window + ImGuiMouseCursor_Hand, // (Unused by Dear ImGui functions. Use for e.g. hyperlinks) + ImGuiMouseCursor_NotAllowed, // When hovering something with disallowed interaction. Usually a crossed circle. + ImGuiMouseCursor_COUNT +}; + +// Enumeration for AddMouseSourceEvent() actual source of Mouse Input data. +// Historically we use "Mouse" terminology everywhere to indicate pointer data, e.g. MousePos, IsMousePressed(), io.AddMousePosEvent() +// But that "Mouse" data can come from different source which occasionally may be useful for application to know about. +// You can submit a change of pointer type using io.AddMouseSourceEvent(). +enum ImGuiMouseSource : int { - return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); -} -static inline void SetNextWindowContentWidth(float w) + ImGuiMouseSource_Mouse = 0, // Input is coming from an actual mouse. + ImGuiMouseSource_TouchScreen, // Input is coming from a touch screen (no hovering prior to initial press, less precise initial press aiming, dual-axis wheeling possible). + ImGuiMouseSource_Pen, // Input is coming from a pressure/magnetic pen (often used in conjunction with high-sampling rates). + ImGuiMouseSource_COUNT +}; + +// Enumeration for ImGui::SetNextWindow***(), SetWindow***(), SetNextItem***() functions +// Represent a condition. +// Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. +enum ImGuiCond_ { - SetNextWindowContentSize(ImVec2(w, 0.0f)); -} -static inline float GetItemsLineHeightWithSpacing() + ImGuiCond_None = 0, // No condition (always set the variable), same as _Always + ImGuiCond_Always = 1 << 0, // No condition (always set the variable), same as _None + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call will succeed) + ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the object/window has no persistently saved data (no entry in .ini file) + ImGuiCond_Appearing = 1 << 3, // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) +}; + +//----------------------------------------------------------------------------- +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +//----------------------------------------------------------------------------- + +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ { - return GetFrameHeightWithSpacing(); -} -// OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) -bool Begin(const char *name, bool *p_open, const ImVec2 &size_on_first_use, float bg_alpha_override = -1.0f, ImGuiWindowFlags flags = 0); // Use SetNextWindowSize(size, ImGuiCond_FirstUseEver) + SetNextWindowBgAlpha() instead. -static inline bool IsRootWindowOrAnyChildHovered() + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + // Miscellaneous + ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, // Highlight column headers when hovered (may evolve into a fuller highlight) + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ { - return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); -} -static inline void AlignFirstTextHeightToWidgets() + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will submit an empty label for this column. Convenient for some small columns. Name will still appear in context menu or in angled headers. You may append into this cell by calling TableSetColumnIndex() right after the TableHeadersRow() call. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + ImGuiTableColumnFlags_AngledHeader = 1 << 18, // TableHeadersRow() will submit an angled header row for this column. Note this will add an extra row. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ { - AlignTextToFramePadding(); -} -static inline void SetNextWindowPosCenter(ImGuiCond c = 0) + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ { - ImGuiIO &io = GetIO(); - SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), c, ImVec2(0.5f, 0.5f)); -} -// OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) -static inline bool IsItemHoveredRect() + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs { - return IsItemHovered(ImGuiHoveredFlags_RectOnly); -} -static inline bool IsPosHoveringAnyWindow(const ImVec2 &) + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs { - IM_ASSERT(0); - return false; -} // This was misleading and partly broken. You probably want to use the ImGui::GetIO().WantCaptureMouse flag instead. -static inline bool IsMouseHoveringAnyWindow() + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: Debug log, memory allocations macros, ImVector<> +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Debug Logging into ShowDebugLogWindow(), tty and more. +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +#define IMGUI_DEBUG_LOG(...) ImGui::DebugLog(__VA_ARGS__) +#else +#define IMGUI_DEBUG_LOG(...) ((void)0) +#endif + +//----------------------------------------------------------------------------- +// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a custom parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +//----------------------------------------------------------------------------- + +struct ImNewWrapper {}; +inline void* operator new(size_t, ImNewWrapper, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewWrapper, void*) {} // This is only required so we can use the symmetrical new() +#define IM_ALLOC(_SIZE) ImGui::MemAlloc(_SIZE) +#define IM_FREE(_PTR) ImGui::MemFree(_PTR) +#define IM_PLACEMENT_NEW(_PTR) new(ImNewWrapper(), _PTR) +#define IM_NEW(_TYPE) new(ImNewWrapper(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +template void IM_DELETE(T* p) { if (p) { p->~T(); ImGui::MemFree(p); } } + +//----------------------------------------------------------------------------- +// ImVector<> +// Lightweight std::vector<>-like class to avoid dragging dependencies (also, some implementations of STL with debug enabled are absurdly slow, we bypass it so our code runs fast in debug). +//----------------------------------------------------------------------------- +// - You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our public structures are relying on it. +// - We use std-like naming convention here, which is a little unusual for this codebase. +// - Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. +// - Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, +// Do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. +//----------------------------------------------------------------------------- + +IM_MSVC_RUNTIME_CHECKS_OFF +template +struct ImVector { - return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); -} -static inline bool IsMouseHoveringWindow() + int Size; + int Capacity; + T* Data; + + // Provide standard typedefs but we don't use them ourselves. + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + // Constructors, destructor + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ImVector(const ImVector& src) { Size = Capacity = 0; Data = NULL; operator=(src); } + inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); if (src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } + inline ~ImVector() { if (Data) IM_FREE(Data); } // Important: does not destruct anything + + inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } // Important: does not destruct anything + inline void clear_delete() { for (int n = 0; n < Size; n++) IM_DELETE(Data[n]); clear(); } // Important: never called automatically! always explicit. + inline void clear_destruct() { for (int n = 0; n < Size; n++) Data[n].~T(); clear(); } // Important: never called automatically! always explicit. + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int size_in_bytes() const { return Size * (int)sizeof(T); } + inline int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); } + inline int capacity() const { return Capacity; } + inline T& operator[](int i) { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + inline const T& operator[](int i) const { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return Data + Size; } + inline const T* end() const { return Data + Size; } + inline T& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const T& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline T& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const T& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; T* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; return new_capacity > sz ? new_capacity : sz; } + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } + inline void shrink(int new_size) { IM_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation + inline void reserve(int new_capacity) { if (new_capacity <= Capacity) return; T* new_data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); if (Data) { memcpy(new_data, Data, (size_t)Size * sizeof(T)); IM_FREE(Data); } Data = new_data; Capacity = new_capacity; } + inline void reserve_discard(int new_capacity) { if (new_capacity <= Capacity) return; if (Data) IM_FREE(Data); Data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); Capacity = new_capacity; } + + // NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the ImVector data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const T& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); memcpy(&Data[Size], &v, sizeof(v)); Size++; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const T& v) { if (Size == 0) push_back(v); else insert(Data, v); } + inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } + inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data + Size && it_last >= it && it_last <= Data + Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - (size_t)count) * sizeof(T)); Size -= (int)count; return Data + off; } + inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; if (it < Data + Size - 1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } + inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data + Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } + inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } + inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline int find_index(const T& v) const { const T* data_end = Data + Size; const T* it = find(v); if (it == data_end) return -1; const ptrdiff_t off = it - Data; return (int)off; } + inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } + inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStyle +//----------------------------------------------------------------------------- +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, use ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, +// and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +//----------------------------------------------------------------------------- + +struct ImGuiStyle { - return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); -} -// OBSOLETED IN 1.49 (between Apr 2016 and May 2016) -static inline bool CollapsingHeader(const char *label, const char *str_id, bool framed = true, bool default_open = false) + float Alpha; // Global alpha applies to everything in Dear ImGui. + float DisabledAlpha; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + ImVec2 WindowPadding; // Padding within a window. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constrain individual windows, use SetNextWindowSizeConstraints(). + ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. + ImGuiDir WindowMenuButtonPosition; // Side of the collapsing/docking button in the title bar (None/Left/Right). Defaults to ImGuiDir_Left. + float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. + float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float PopupRounding; // Radius of popup window corners rounding. (Note that tooltip windows use WindowRounding) + float PopupBorderSize; // Thickness of border around popup/tooltip windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). + float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). + float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. + ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. + float ScrollbarRounding; // Radius of grab corners for scrollbar. + float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. + float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + float LogSliderDeadzone; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + float TabBorderSize; // Thickness of border around tabs. + float TabMinWidthForCloseButton; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + float TabBarOverlineSize; // Thickness of tab-bar overline, which highlights the selected tab-bar. + float TableAngledHeadersAngle; // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees). + ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell + ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + float SeparatorTextBorderSize; // Thickness of border in SeparatorText() + ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + ImVec2 DisplayWindowPadding; // Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen. + ImVec2 DisplaySafeAreaPadding; // Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured). + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). We apply per-monitor DPI scaling over this scale. May be removed later. + bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + ImVec4 Colors[ImGuiCol_COUNT]; + + // Behaviors + // (It is possible to modify those fields mid-frame if specific behavior need it, unlike e.g. configuration fields in ImGuiIO) + float HoverStationaryDelay; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + float HoverDelayShort; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + float HoverDelayNormal; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + ImGuiHoveredFlags HoverFlagsForTooltipNav; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + + IMGUI_API ImGuiStyle(); + IMGUI_API void ScaleAllSizes(float scale_factor); +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiIO +//----------------------------------------------------------------------------- +// Communicate most settings and inputs/outputs to Dear ImGui using this structure. +// Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +// It is generally expected that: +// - initialization: backends and user code writes to ImGuiIO. +// - main loop: backends writes to ImGuiIO, user code and imgui code reads from ImGuiIO. +//----------------------------------------------------------------------------- +// Also see ImGui::GetPlatformIO() and ImGuiPlatformIO struct for OS/platform related functions: clipboard, IME etc. +//----------------------------------------------------------------------------- + +// [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. +// If prior to 1.87 you used io.KeysDownDuration[] (which was marked as internal), you should use GetKeyData(key)->DownDuration and *NOT* io.KeysData[key]->DownDuration. +struct ImGuiKeyData { - (void) str_id; - (void) framed; - ImGuiTreeNodeFlags default_open_flags = 1 << 5; - return CollapsingHeader(label, (default_open ? default_open_flags : 0)); -} -} // namespace ImGui + bool Down; // True for if key is down + float DownDuration; // Duration the key has been down (<0.0f: not pressed, 0.0f: just pressed, >0.0f: time held) + float DownDurationPrev; // Last frame duration the key has been down + float AnalogValue; // 0.0f..1.0f for gamepad values +}; + +struct ImGuiIO +{ + //------------------------------------------------------------------ + // Configuration // Default value + //------------------------------------------------------------------ + + ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Gamepad/keyboard navigation options, etc. + ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend. + ImVec2 DisplaySize; // // Main display size, in pixels (generally == GetMainViewport()->Size). May change every frame. + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. May change every frame. + float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. + const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. + const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). + void* UserData; // = NULL // Store your own data. + + ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. + float FontGlobalScale; // = 1.0f // Global scale all fonts + bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. + ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. + ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + + // Miscellaneous options + bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. + bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // Swap Cmd<>Ctrl keys + OS X style text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. + bool ConfigNavSwapGamepadButtons; // = false // Swap Activate<>Cancel (A<>B) buttons, matching typical "Nintendo/Japanese style" gamepad layout. + bool ConfigInputTrickleEventQueue; // = true // Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates. + bool ConfigInputTextCursorBlink; // = true // Enable blinking cursor (optional as some users consider it to be distracting). + bool ConfigInputTextEnterKeepActive; // = false // [BETA] Pressing Enter will keep item active and select contents (single-line only). + bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) + bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. + float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + + // Inputs Behaviors + // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + + //------------------------------------------------------------------ + // Debug options + //------------------------------------------------------------------ + + // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. + // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. + // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. + // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + bool ConfigDebugIsDebuggerPresent; // = false // Enable various tools calling IM_DEBUG_BREAK(). + + // Tools to test correct Begin/End and BeginChild/EndChild behaviors. + // - Presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() + // - This is inconsistent with other BeginXXX functions and create confusion for many users. + // - We expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. + bool ConfigDebugBeginReturnValueOnce;// = false // First-time calls to Begin()/BeginChild() will return false. NEEDS TO BE SET AT APPLICATION BOOT TIME if you don't want to miss windows. + bool ConfigDebugBeginReturnValueLoop;// = false // Some calls to Begin()/BeginChild() will return false. Will cycle through window depths then repeat. Suggested use: add "io.ConfigDebugBeginReturnValue = io.KeyShift" in your main loop then occasionally press SHIFT. Windows should be flickering while running. + + // Option to deactivate io.AddFocusEvent(false) handling. + // - May facilitate interactions with a debugger when focus loss leads to clearing inputs data. + // - Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. + bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys()/io.ClearInputMouse() in input processing. + + // Option to audit .ini data + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) + + //------------------------------------------------------------------ + // Platform Functions + // (the imgui_impl_xxxx backend files are setting those up for you) + //------------------------------------------------------------------ + + // Optional: Platform/Renderer backend name (informational only! will be displayed in About Window) + User data for backend/wrappers to store their own stuff. + const char* BackendPlatformName; // = NULL + const char* BackendRendererName; // = NULL + void* BackendPlatformUserData; // = NULL // User data for platform backend + void* BackendRendererUserData; // = NULL // User data for renderer backend + void* BackendLanguageUserData; // = NULL // User data for non C++ programming language backend + + //------------------------------------------------------------------ + // Input - Call before calling NewFrame() + //------------------------------------------------------------------ + + // Input Functions + IMGUI_API void AddKeyEvent(ImGuiKey key, bool down); // Queue a new key down/up event. Key should be "translated" (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) + IMGUI_API void AddKeyAnalogEvent(ImGuiKey key, bool down, float v); // Queue a new key down/up event for analog values (e.g. ImGuiKey_Gamepad_ values). Dead-zones should be handled by the backend. + IMGUI_API void AddMousePosEvent(float x, float y); // Queue a mouse position update. Use -FLT_MAX,-FLT_MAX to signify no mouse (e.g. app not focused and not hovered) + IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change + IMGUI_API void AddMouseWheelEvent(float wheel_x, float wheel_y); // Queue a mouse wheel update. wheel_y<0: scroll down, wheel_y>0: scroll up, wheel_x<0: scroll right, wheel_x>0: scroll left. + IMGUI_API void AddMouseSourceEvent(ImGuiMouseSource source); // Queue a mouse source change (Mouse/TouchScreen/Pen) + IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) + IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input + IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from a UTF-16 character, it can be a surrogate + IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue a new characters input from a UTF-8 string + + IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. + IMGUI_API void SetAppAcceptingEvents(bool accepting_events); // Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. + IMGUI_API void ClearEventsQueue(); // Clear all incoming events. + IMGUI_API void ClearInputKeys(); // Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. + IMGUI_API void ClearInputMouse(); // Clear current mouse state. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + IMGUI_API void ClearInputCharacters(); // [Obsoleted in 1.89.8] Clear the current frame text input buffer. Now included within ClearInputKeys(). +#endif + + //------------------------------------------------------------------ + // Output - Updated by NewFrame() or EndFrame()/Render() + // (when reading from the io.WantCaptureMouse, io.WantCaptureKeyboard flags to dispatch your inputs, it is + // generally easier and more correct to use their state BEFORE calling NewFrame(). See FAQ for details!) + //------------------------------------------------------------------ + + bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them to your main game/application (either way, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). + bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, in this case do not dispatch them to your main game/application (either way, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). + bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard. This is set by Dear ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantSetMousePos; // MousePos has been altered, backend should reposition mouse on next frame. Rarely used! Set only when ImGuiConfigFlags_NavEnableSetMousePos flag is enabled. + bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. Important: clear io.WantSaveIniSettings yourself after saving! + bool NavActive; // Keyboard/Gamepad navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Keyboard/Gamepad navigation is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Estimate of application framerate (rolling average over 60 frames, based on io.DeltaTime), in frame per second. Solely for convenience. Slow applications may not want to use a moving average or may want to reset underlying buffers occasionally. + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsRenderWindows; // Number of visible windows + int MetricsActiveWindows; // Number of active windows + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + + //------------------------------------------------------------------ + // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! + //------------------------------------------------------------------ + + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + + // Main Input State + // (this block used to be written by backend, since 1.87 it is best to NOT write to those directly, call the AddXXX functions above instead) + // (reading from those variables is fair game, as they are extremely unlikely to be moving anywhere) + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX, -FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras (ImGuiMouseButton_COUNT == 5). Dear ImGui mostly uses left and right buttons. Other buttons allow us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll. + float MouseWheelH; // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends. + ImGuiMouseSource MouseSource; // Mouse actual input peripheral (Mouse/TouchScreen/Pen). + bool KeyCtrl; // Keyboard modifier down: Control + bool KeyShift; // Keyboard modifier down: Shift + bool KeyAlt; // Keyboard modifier down: Alt + bool KeySuper; // Keyboard modifier down: Cmd/Super/Windows + + // Other state maintained from data above + IO function calls + ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. Read-only, updated by NewFrame() + ImGuiKeyData KeysData[ImGuiKey_KeysData_SIZE]; // Key state for all known keys. Use IsKeyXXX() functions to access this. + bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. + ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + double MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down (same as MouseClickedCount[x] != 0) + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? (same as MouseClickedCount[x] == 2) + ImU16 MouseClickedCount[5]; // == 0 (not clicked), == 1 (same as MouseClicked[]), == 2 (double-clicked), == 3 (triple-clicked) etc. when going from !Down to Down + ImU16 MouseClickedLastCount[5]; // Count successive number of clicks. Stays valid after mouse release. Reset after another click is done. + bool MouseReleased[5]; // Mouse button went from Down to !Down + bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window or over void blocked by a popup. We don't request mouse capture from the application if click started outside ImGui bounds. + bool MouseDownOwnedUnlessPopupClose[5]; // Track if button was clicked inside a dear imgui window. + bool MouseWheelRequestAxisSwap; // On a non-Mac system, holding SHIFT requests WheelY to perform the equivalent of a WheelX event. On a Mac system this is already enforced by the system. + bool MouseCtrlLeftAsRightClick; // (OSX) Set to true when the current click was a ctrl-click that spawned a simulated right click + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) + float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. + bool AppFocusLost; // Only modify via AddFocusEvent() + bool AppAcceptingEvents; // Only modify via SetAppAcceptingEvents() + ImS8 BackendUsingLegacyKeyArrays; // -1: unknown, 0: using AddKeyEvent(), 1: using legacy io.KeysDown[] + bool BackendUsingLegacyNavInputArray; // 0: using AddKeyAnalogEvent(), 1: writing to legacy io.NavInputs[] directly + ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() + ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). + // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. + float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. + //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. +#endif + + // Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO. + // As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; #endif + IMGUI_API ImGuiIO(); +}; + //----------------------------------------------------------------------------- -// Helpers +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) //----------------------------------------------------------------------------- -// Lightweight std::vector<> like class to avoid dragging dependencies (also: windows implementation of STL with debug enabled is absurdly slow, so let's bypass it so our code runs fast in debug). -// Our implementation does NOT call C++ constructors/destructors. This is intentional and we do not require it. Do not use this class as a straight std::vector replacement in your code! -template -class ImVector -{ - public: - int Size; - int Capacity; - T *Data; - - typedef T value_type; - typedef value_type *iterator; - typedef const value_type *const_iterator; - - inline ImVector() - { - Size = Capacity = 0; - Data = NULL; - } - inline ~ImVector() - { - if (Data) - ImGui::MemFree(Data); - } - - inline bool empty() const - { - return Size == 0; - } - inline int size() const - { - return Size; - } - inline int capacity() const - { - return Capacity; - } - - inline value_type &operator[](int i) - { - IM_ASSERT(i < Size); - return Data[i]; - } - inline const value_type &operator[](int i) const - { - IM_ASSERT(i < Size); - return Data[i]; - } - - inline void clear() - { - if (Data) - { - Size = Capacity = 0; - ImGui::MemFree(Data); - Data = NULL; - } - } - inline iterator begin() - { - return Data; - } - inline const_iterator begin() const - { - return Data; - } - inline iterator end() - { - return Data + Size; - } - inline const_iterator end() const - { - return Data + Size; - } - inline value_type &front() - { - IM_ASSERT(Size > 0); - return Data[0]; - } - inline const value_type &front() const - { - IM_ASSERT(Size > 0); - return Data[0]; - } - inline value_type &back() - { - IM_ASSERT(Size > 0); - return Data[Size - 1]; - } - inline const value_type &back() const - { - IM_ASSERT(Size > 0); - return Data[Size - 1]; - } - inline void swap(ImVector &rhs) - { - int rhs_size = rhs.Size; - rhs.Size = Size; - Size = rhs_size; - int rhs_cap = rhs.Capacity; - rhs.Capacity = Capacity; - Capacity = rhs_cap; - value_type *rhs_data = rhs.Data; - rhs.Data = Data; - Data = rhs_data; - } - - inline int _grow_capacity(int sz) const - { - int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; - return new_capacity > sz ? new_capacity : sz; - } - - inline void resize(int new_size) - { - if (new_size > Capacity) - reserve(_grow_capacity(new_size)); - Size = new_size; - } - inline void resize(int new_size, const T &v) - { - if (new_size > Capacity) - reserve(_grow_capacity(new_size)); - if (new_size > Size) - for (int n = Size; n < new_size; n++) - Data[n] = v; - Size = new_size; - } - inline void reserve(int new_capacity) - { - if (new_capacity <= Capacity) - return; - T *new_data = (value_type *) ImGui::MemAlloc((size_t) new_capacity * sizeof(T)); - if (Data) - memcpy(new_data, Data, (size_t) Size * sizeof(T)); - ImGui::MemFree(Data); - Data = new_data; - Capacity = new_capacity; - } - - // NB: &v cannot be pointing inside the ImVector Data itself! e.g. v.push_back(v[10]) is forbidden. - inline void push_back(const value_type &v) - { - if (Size == Capacity) - reserve(_grow_capacity(Size + 1)); - Data[Size++] = v; - } - inline void pop_back() - { - IM_ASSERT(Size > 0); - Size--; - } - inline void push_front(const value_type &v) - { - if (Size == 0) - push_back(v); - else - insert(Data, v); - } - - inline iterator erase(const_iterator it) - { - IM_ASSERT(it >= Data && it < Data + Size); - const ptrdiff_t off = it - Data; - memmove(Data + off, Data + off + 1, ((size_t) Size - (size_t) off - 1) * sizeof(value_type)); - Size--; - return Data + off; - } - inline iterator insert(const_iterator it, const value_type &v) - { - IM_ASSERT(it >= Data && it <= Data + Size); - const ptrdiff_t off = it - Data; - if (Size == Capacity) - reserve(_grow_capacity(Size + 1)); - if (off < (int) Size) - memmove(Data + off + 1, Data + off, ((size_t) Size - (size_t) off) * sizeof(value_type)); - Data[off] = v; - Size++; - return Data + off; - } - inline bool contains(const value_type &v) const - { - const T *data = Data; - const T *data_end = Data + Size; - while (data < data_end) - if (*data++ == v) - return true; - return false; - } -}; - -// Helper: execute a block of code at maximum once a frame. Convenient if you want to quickly create an UI within deep-nested code that runs multiple times every frame. -// Usage: -// static ImGuiOnceUponAFrame oaf; -// if (oaf) -// ImGui::Text("This will be called only once per frame"); -struct ImGuiOnceUponAFrame +// Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. +// The callback function should return 0 by default. +// Callbacks (follow a flag name and see comments in ImGuiInputTextFlags_ declarations for more details) +// - ImGuiInputTextFlags_CallbackEdit: Callback on buffer edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) +// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration +// - ImGuiInputTextFlags_CallbackCompletion: Callback on pressing TAB +// - ImGuiInputTextFlags_CallbackHistory: Callback on pressing Up/Down arrows +// - ImGuiInputTextFlags_CallbackCharFilter: Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. +// - ImGuiInputTextFlags_CallbackResize: Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. +struct ImGuiInputTextCallbackData +{ + ImGuiContext* Ctx; // Parent UI context + ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + + // Arguments for the different callback events + // - During Resize callback, Buf will be same as your input buffer. + // - However, during Completion/History/Always callback, Buf always points to our own internal data (it is not the same as your buffer)! Changes to it will be reflected into your own buffer shortly after the callback. + // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. + // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. + ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only // [Completion,History] + char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! + int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() + int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 + bool BufDirty; // Set if you modify Buf/BufTextLen! // Write // [Completion,History,Always] + int CursorPos; // // Read-write // [Completion,History,Always] + int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection) + int SelectionEnd; // // Read-write // [Completion,History,Always] + + // Helper functions for text manipulation. + // Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection. + IMGUI_API ImGuiInputTextCallbackData(); + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + void SelectAll() { SelectionStart = 0; SelectionEnd = BufTextLen; } + void ClearSelection() { SelectionStart = SelectionEnd = BufTextLen; } + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). +// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. +struct ImGuiSizeCallbackData { - ImGuiOnceUponAFrame() - { - RefFrame = -1; - } - mutable int RefFrame; - operator bool() const - { - int current_frame = ImGui::GetFrameCount(); - if (RefFrame == current_frame) - return false; - RefFrame = current_frame; - return true; - } -}; - -// Helper macro for ImGuiOnceUponAFrame. Attention: The macro expands into 2 statement so make sure you don't use it within e.g. an if() statement without curly braces. -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Will obsolete -# define IMGUI_ONCE_UPON_A_FRAME \ - static ImGuiOnceUponAFrame imgui_oaf; \ - if (imgui_oaf) + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints(). Generally store an integer or float in here (need reinterpret_cast<>). + ImVec2 Pos; // Read-only. Window position, for reference. + ImVec2 CurrentSize; // Read-only. Current window size. + ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. +}; + +// Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() +struct ImGuiPayload +{ + // Members + void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[32 + 1]; // Data type tag (short user-supplied string, 32 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +//----------------------------------------------------------------------------- + +// Helper: Unicode defines +#define IM_UNICODE_CODEPOINT_INVALID 0xFFFD // Invalid Unicode code point (standard value). +#ifdef IMGUI_USE_WCHAR32 +#define IM_UNICODE_CODEPOINT_MAX 0x10FFFF // Maximum Unicode code point supported by this build. +#else +#define IM_UNICODE_CODEPOINT_MAX 0xFFFF // Maximum Unicode code point supported by this build. #endif +// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create a UI within deep-nested code that runs multiple times every frame. +// Usage: static ImGuiOnceUponAFrame oaf; if (oaf) ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + ImGuiOnceUponAFrame() { RefFrame = -1; } + mutable int RefFrame; + operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; } +}; + // Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" struct ImGuiTextFilter { - struct TextRange - { - const char *b; - const char *e; - - TextRange() - { - b = e = NULL; - } - TextRange(const char *_b, const char *_e) - { - b = _b; - e = _e; - } - const char *begin() const - { - return b; - } - const char *end() const - { - return e; - } - bool empty() const - { - return b == e; - } - char front() const - { - return *b; - } - static bool is_blank(char c) - { - return c == ' ' || c == '\t'; - } - void trim_blanks() - { - while (b < e && is_blank(*b)) - b++; - while (e > b && is_blank(*(e - 1))) - e--; - } - IMGUI_API void split(char separator, ImVector &out); - }; - - char InputBuf[256]; - ImVector Filters; - int CountGrep; - - IMGUI_API ImGuiTextFilter(const char *default_filter = ""); - IMGUI_API bool Draw(const char *label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build - IMGUI_API bool PassFilter(const char *text, const char *text_end = NULL) const; - IMGUI_API void Build(); - void Clear() - { - InputBuf[0] = 0; - Build(); - } - bool IsActive() const - { - return !Filters.empty(); - } -}; - -// Helper: Text buffer for logging/accumulating text + IMGUI_API ImGuiTextFilter(const char* default_filter = ""); + IMGUI_API bool Draw(const char* label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build + IMGUI_API bool PassFilter(const char* text, const char* text_end = NULL) const; + IMGUI_API void Build(); + void Clear() { InputBuf[0] = 0; Build(); } + bool IsActive() const { return !Filters.empty(); } + + // [Internal] + struct ImGuiTextRange + { + const char* b; + const char* e; + + ImGuiTextRange() { b = e = NULL; } + ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; } + bool empty() const { return b == e; } + IMGUI_API void split(char separator, ImVector* out) const; + }; + char InputBuf[256]; + ImVectorFilters; + int CountGrep; +}; + +// Helper: Growable text buffer for logging/accumulating text +// (this could be called 'ImGuiTextBuilder' / 'ImGuiStringBuilder') struct ImGuiTextBuffer { - ImVector Buf; - - ImGuiTextBuffer() - { - Buf.push_back(0); - } - inline char operator[](int i) - { - return Buf.Data[i]; - } - const char *begin() const - { - return &Buf.front(); - } - const char *end() const - { - return &Buf.back(); - } // Buf is zero-terminated, so end() will point on the zero-terminator - int size() const - { - return Buf.Size - 1; - } - bool empty() - { - return Buf.Size <= 1; - } - void clear() - { - Buf.clear(); - Buf.push_back(0); - } - void reserve(int capacity) - { - Buf.reserve(capacity); - } - const char *c_str() const - { - return Buf.Data; - } - IMGUI_API void appendf(const char *fmt, ...) IM_FMTARGS(2); - IMGUI_API void appendfv(const char *fmt, va_list args) IM_FMTLIST(2); -}; - -// Helper: Simple Key->value storage + ImVector Buf; + IMGUI_API static char EmptyString[1]; + + ImGuiTextBuffer() { } + inline char operator[](int i) const { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } + const char* begin() const { return Buf.Data ? &Buf.front() : EmptyString; } + const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator + int size() const { return Buf.Size ? Buf.Size - 1 : 0; } + bool empty() const { return Buf.Size <= 1; } + void clear() { Buf.clear(); } + void reserve(int capacity) { Buf.reserve(capacity); } + const char* c_str() const { return Buf.Data ? Buf.Data : EmptyString; } + IMGUI_API void append(const char* str, const char* str_end = NULL); + IMGUI_API void appendf(const char* fmt, ...) IM_FMTARGS(2); + IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); +}; + +// [Internal] Key+Value for ImGuiStorage +struct ImGuiStoragePair +{ + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + ImGuiStoragePair(ImGuiID _key, int _val) { key = _key; val_i = _val; } + ImGuiStoragePair(ImGuiID _key, float _val) { key = _key; val_f = _val; } + ImGuiStoragePair(ImGuiID _key, void* _val) { key = _key; val_p = _val; } +}; + +// Helper: Key->Value storage // Typically you don't have to worry about this since a storage is held within each Window. -// We use it to e.g. store collapse state for a tree (Int 0/1), store color edit options. -// This is optimized for efficient reading (dichotomy into a contiguous buffer), rare writing (typically tied to user interactions) +// We use it to e.g. store collapse state for a tree (Int 0/1) +// This is optimized for efficient lookup (dichotomy into a contiguous buffer) and rare insertion (typically tied to user interactions aka max once a frame) // You can use it as custom user storage for temporary values. Declare your own storage if, for example: // - You want to manipulate the open/close state of a particular sub-tree in your interface (tree node uses Int 0/1 to store their state). // - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) // Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. struct ImGuiStorage { - struct Pair - { - ImGuiID key; - union - { - int val_i; - float val_f; - void *val_p; - }; - Pair(ImGuiID _key, int _val_i) - { - key = _key; - val_i = _val_i; - } - Pair(ImGuiID _key, float _val_f) - { - key = _key; - val_f = _val_f; - } - Pair(ImGuiID _key, void *_val_p) - { - key = _key; - val_p = _val_p; - } - }; - ImVector Data; - - // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) - // - Set***() functions find pair, insertion on demand if missing. - // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. - void Clear() - { - Data.clear(); - } - IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; - IMGUI_API void SetInt(ImGuiID key, int val); - IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; - IMGUI_API void SetBool(ImGuiID key, bool val); - IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; - IMGUI_API void SetFloat(ImGuiID key, float val); - IMGUI_API void *GetVoidPtr(ImGuiID key) const; // default_val is NULL - IMGUI_API void SetVoidPtr(ImGuiID key, void *val); - - // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. - // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. - // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) - // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; - IMGUI_API int *GetIntRef(ImGuiID key, int default_val = 0); - IMGUI_API bool *GetBoolRef(ImGuiID key, bool default_val = false); - IMGUI_API float *GetFloatRef(ImGuiID key, float default_val = 0.0f); - IMGUI_API void **GetVoidPtrRef(ImGuiID key, void *default_val = NULL); - - // Use on your own storage if you know only integer are being stored (open/close all tree nodes) - IMGUI_API void SetAllInt(int val); - - // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. - IMGUI_API void BuildSortByKey(); -}; - -// Shared state of InputText(), passed to callback when a ImGuiInputTextFlags_Callback* flag is used and the corresponding callback is triggered. -struct ImGuiTextEditCallbackData -{ - ImGuiInputTextFlags EventFlag; // One of ImGuiInputTextFlags_Callback* // Read-only - ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only - void *UserData; // What user passed to InputText() // Read-only - bool ReadOnly; // Read-only mode // Read-only - - // CharFilter event: - ImWchar EventChar; // Character input // Read-write (replace character or set to zero) - - // Completion,History,Always events: - // If you modify the buffer contents make sure you update 'BufTextLen' and set 'BufDirty' to true. - ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only - char *Buf; // Current text buffer // Read-write (pointed data only, can't replace the actual pointer) - int BufTextLen; // Current text length in bytes // Read-write - int BufSize; // Maximum text length in bytes // Read-only - bool BufDirty; // Set if you modify Buf/BufTextLen!! // Write - int CursorPos; // // Read-write - int SelectionStart; // // Read-write (== to SelectionEnd when no selection) - int SelectionEnd; // // Read-write - - // NB: Helper functions for text manipulation. Calling those function loses selection. - IMGUI_API void DeleteChars(int pos, int bytes_count); - IMGUI_API void InsertChars(int pos, const char *text, const char *text_end = NULL); - bool HasSelection() const - { - return SelectionStart != SelectionEnd; - } + // [Internal] + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Advanced: for quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); + // Obsolete: use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //typedef ::ImGuiStoragePair ImGuiStoragePair; // 1.90.8: moved type outside struct +#endif }; -// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). -// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. -struct ImGuiSizeCallbackData +// Helper: Manually clip large list of items. +// If you have lots evenly spaced items and you have random access to the list, you can perform coarse +// clipping based on visibility to only submit items that are in view. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally +// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily +// scale using lists with tens of thousands of items without a problem) +// Usage: +// ImGuiListClipper clipper; +// clipper.Begin(1000); // We have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// Generally what happens is: +// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. +// - User code submit that one element. +// - Clipper can measure the height of the first element +// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. +// - User code submit visible elements. +// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc. +struct ImGuiListClipper { - void *UserData; // Read-only. What user passed to SetNextWindowSizeConstraints() - ImVec2 Pos; // Read-only. Window position, for reference. - ImVec2 CurrentSize; // Read-only. Current window size. - ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. + ImGuiContext* Ctx; // Parent UI context + int DisplayStart; // First item to display, updated by each call to Step() + int DisplayEnd; // End of items to display (exclusive) + int ItemsCount; // [Internal] Number of items + float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it + float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed + double StartSeekOffsetY; // [Internal] Account for frozen rows in a table and initial loss of precision in very large windows. + void* TempData; // [Internal] Internal data + + // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step, and you can call SeekCursorForItem() manually if you need) + // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). + IMGUI_API ImGuiListClipper(); + IMGUI_API ~ImGuiListClipper(); + IMGUI_API void Begin(int items_count, float items_height = -1.0f); + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. + + // Call IncludeItemByIndex() or IncludeItemsByIndex() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. + // (Due to alignment / padding of certain items it is possible that an extra item may be included on either end of the display range). + inline void IncludeItemByIndex(int item_index) { IncludeItemsByIndex(item_index, item_index + 1); } + IMGUI_API void IncludeItemsByIndex(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. + + // Seek cursor toward given item. This is automatically called while stepping. + // - The only reason to call this is: you can use ImGuiListClipper::Begin(INT_MAX) if you don't know item count ahead of time. + // - In this case, after all steps are done, you'll want to call SeekCursorForItem(item_count). + IMGUI_API void SeekCursorForItem(int item_index); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void IncludeRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9] + inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6] + //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] +#endif }; -// Data payload for Drag and Drop operations -struct ImGuiPayload -{ - // Members - const void *Data; // Data (copied and owned by dear imgui) - int DataSize; // Data size - - // [Internal] - ImGuiID SourceId; // Source item id - ImGuiID SourceParentId; // Source parent id (if available) - int DataFrameCount; // Data timestamp - char DataType[12 + 1]; // Data type tag (short user-supplied string, 12 characters max) - bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) - bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. - - ImGuiPayload() - { - Clear(); - } - void Clear() - { - SourceId = SourceParentId = 0; - Data = NULL; - DataSize = 0; - memset(DataType, 0, sizeof(DataType)); - DataFrameCount = -1; - Preview = Delivery = false; - } - bool IsDataType(const char *type) const - { - return DataFrameCount != -1 && strcmp(type, DataType) == 0; - } - bool IsPreview() const - { - return Preview; - } - bool IsDelivery() const - { - return Delivery; - } -}; - -// Helpers macros to generate 32-bits encoded colors +// Helpers: ImVec2/ImVec4 operators +// - It is important that we are keeping those disabled by default so they don't leak in user space. +// - This is in order to allow user enabling implicit cast operators between ImVec2/ImVec4 and their own types (using IM_VEC2_CLASS_EXTRA in imconfig.h) +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +#ifdef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +IM_MSVC_RUNTIME_CHECKS_OFF +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +static inline bool operator==(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; } +static inline bool operator!=(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; } +IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Helpers macros to generate 32-bit encoded colors +// User can declare their own format by #defining the 5 _SHIFT/_MASK macros in their imconfig file. +#ifndef IM_COL32_R_SHIFT #ifdef IMGUI_USE_BGRA_PACKED_COLOR -# define IM_COL32_R_SHIFT 16 -# define IM_COL32_G_SHIFT 8 -# define IM_COL32_B_SHIFT 0 -# define IM_COL32_A_SHIFT 24 -# define IM_COL32_A_MASK 0xFF000000 +#define IM_COL32_R_SHIFT 16 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 0 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 #else -# define IM_COL32_R_SHIFT 0 -# define IM_COL32_G_SHIFT 8 -# define IM_COL32_B_SHIFT 16 -# define IM_COL32_A_SHIFT 24 -# define IM_COL32_A_MASK 0xFF000000 +#define IM_COL32_R_SHIFT 0 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 16 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 #endif -#define IM_COL32(R, G, B, A) (((ImU32) (A) << IM_COL32_A_SHIFT) | ((ImU32) (B) << IM_COL32_B_SHIFT) | ((ImU32) (G) << IM_COL32_G_SHIFT) | ((ImU32) (R) << IM_COL32_R_SHIFT)) -#define IM_COL32_WHITE IM_COL32(255, 255, 255, 255) // Opaque white = 0xFFFFFFFF -#define IM_COL32_BLACK IM_COL32(0, 0, 0, 255) // Opaque black -#define IM_COL32_BLACK_TRANS IM_COL32(0, 0, 0, 0) // Transparent black = 0x00000000 +#endif +#define IM_COL32(R,G,B,A) (((ImU32)(A)<> IM_COL32_R_SHIFT) & 0xFF) * sc; - Value.y = (float) ((rgba >> IM_COL32_G_SHIFT) & 0xFF) * sc; - Value.z = (float) ((rgba >> IM_COL32_B_SHIFT) & 0xFF) * sc; - Value.w = (float) ((rgba >> IM_COL32_A_SHIFT) & 0xFF) * sc; - } - ImColor(float r, float g, float b, float a = 1.0f) - { - Value.x = r; - Value.y = g; - Value.z = b; - Value.w = a; - } - ImColor(const ImVec4 &col) - { - Value = col; - } - inline operator ImU32() const - { - return ImGui::ColorConvertFloat4ToU32(Value); - } - inline operator ImVec4() const - { - return Value; - } - - // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. - inline void SetHSV(float h, float s, float v, float a = 1.0f) - { - ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); - Value.w = a; - } - static ImColor HSV(float h, float s, float v, float a = 1.0f) - { - float r, g, b; - ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); - return ImColor(r, g, b, a); - } + ImVec4 Value; + + constexpr ImColor() { } + constexpr ImColor(float r, float g, float b, float a = 1.0f) : Value(r, g, b, a) { } + constexpr ImColor(const ImVec4& col) : Value(col) {} + constexpr ImColor(int r, int g, int b, int a = 255) : Value((float)r * (1.0f / 255.0f), (float)g * (1.0f / 255.0f), (float)b * (1.0f / 255.0f), (float)a* (1.0f / 255.0f)) {} + constexpr ImColor(ImU32 rgba) : Value((float)((rgba >> IM_COL32_R_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * (1.0f / 255.0f)) {} + inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } + inline operator ImVec4() const { return Value; } + + // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. + inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } }; -// Helper: Manually clip large list of items. -// If you are submitting lots of evenly spaced items and you have a random access to the list, you can perform coarse clipping based on visibility to save yourself from processing those items at all. -// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. -// ImGui already clip items based on their bounds but it needs to measure text size to do so. Coarse clipping before submission makes this cost and your own data fetching/submission cost null. +//----------------------------------------------------------------------------- +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) +//----------------------------------------------------------------------------- + +// Multi-selection system +// Documentation at: https://github.com/ocornut/imgui/wiki/Multi-Select +// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. +// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) +// with support for clipper (skipping non-visible items), box-select and many other details. +// - Selectable(), Checkbox() are supported but custom widgets may use it as well. +// - TreeNode() is technically supported but... using this correctly is more complicated: you need some sort of linear/random access to your tree, +// which is suited to advanced trees setups also implementing filters and clipper. We will work toward simplifying and demoing it. +// - In the spirit of Dear ImGui design, your code owns actual selection data. +// This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience to quickly implement multi-selection. // Usage: -// ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced. -// while (clipper.Step()) -// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) -// ImGui::Text("line number %d", i); -// - Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height (step skipped if we passed a known height as second arg to constructor). -// - Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. -// - (Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user call Step(). Does nothing and switch to Step 3.) -// - Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. -struct ImGuiListClipper +// - Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set. +// - Store and maintain actual selection data using persistent object identifiers. +// - Usage flow: +// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (2) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. +// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (6) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 2. +// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. +// About ImGuiSelectionUserData: +// - This can store an application-defined identifier (e.g. index or pointer) submitted via SetNextItemSelectionUserData(). +// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. +// - Most applications will store an object INDEX, hence the chosen name and type. Storing an index is natural, because +// SetRange requests will give you two end-points and you will need to iterate/interpolate between them to update your selection. +// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside ImGuiSelectionUserData. +// Our system never assume that you identify items by indices, it never attempts to interpolate between two values. +// - If you enable ImGuiMultiSelectFlags_NoRangeSelect then it is guaranteed that you will never have to interpolate +// between two ImGuiSelectionUserData, which may be a convenient way to use part of the feature with less code work. +// - As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, +// being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside. + +// Flags for BeginMultiSelect() +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_None = 0, + ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. + ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). With BoxSelect is also ensure contiguous SetRange requests are not combined into one. This allows not handling interpolation in SetRange requests. + ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5, // Disable clearing selection when clicking/selecting an already selected item. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. full row Selectable()). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This is slower: alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 8, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 9, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 10, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 11, // Scope for _BoxSelect and _ClearOnClickVoid is whole window (Default). Use if BeginMultiSelect() covers a whole window or used a single time in same window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 12, // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + //ImGuiMultiSelectFlags_RangeSelect2d = 1 << 15, // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does. + ImGuiMultiSelectFlags_NavWrapX = 1 << 16, // [Temporary] Enable navigation wrapping on X axis. Provided as a convenience because we don't have a design for the general Nav API for this yet. When the more general feature be public we may obsolete this flag in favor of new one. +}; + +// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). +// This mainly contains a list of selection requests. +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only useful if your list is dynamic and allows deletion (getting post-deletion focus/state right is shown in the demo) +// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. +struct ImGuiMultiSelectIO +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (often the first selected item) must never be clipped: use clipper.IncludeItemByIndex() to ensure it is submitted. + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + int ItemsCount; // ms:w, app:r / app:r // 'int items_count' parameter to BeginMultiSelect() is copied here for convenience, allowing simpler calls to your ApplyRequests handler. Not used internally. +}; + +// Selection request type +enum ImGuiSelectionRequestType +{ + ImGuiSelectionRequestType_None = 0, + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true). We cannot set RangeFirstItem/RangeLastItem as its contents is entirely up to user (not necessarily an index) + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. +}; + +// Selection request item +struct ImGuiSelectionRequest +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. + bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) + ImS8 RangeDirection; // / ms:w app:r // Parameter for SetRange request: +1 when RangeFirstItem comes before RangeLastItem, -1 otherwise. Useful if you want to preserve selection order on a backward Shift+Click. + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom). + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top). Inclusive! +}; + +// Optional helper to store multi-selection state + apply multi-selection requests. +// - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' +// Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. +// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. +// To store a multi-selection, in your application you could: +// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - Use your own external storage: e.g. std::set, std::vector, interval trees, intrusively stored selection etc. +// In ImGuiSelectionBasicStorage we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - use decently optimized logic to allow queries and insertion of very large selection sets. +// - do not preserve selection order. +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. +// See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. +struct ImGuiSelectionBasicStorage { - float StartPosY; - float ItemsHeight; - int ItemsCount, StepNo, DisplayStart, DisplayEnd; + // Members + int Size; // // Number of selected items, maintained by this helper. + bool PreserveOrder; // = false // GetNextSelectedItem() will return ordered selection (currently implemented by two additional sorts of selection. Could be improved) + void* UserData; // = NULL // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; + int _SelectionOrder;// [Internal] Increasing counter to store selection order + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). + + // Methods + IMGUI_API ImGuiSelectionBasicStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() + IMGUI_API bool Contains(ImGuiID id) const; // Query if an item id is in selection. + IMGUI_API void Clear(); // Clear selection + IMGUI_API void Swap(ImGuiSelectionBasicStorage& r); // Swap two selections + IMGUI_API void SetItemSelected(ImGuiID id, bool selected); // Add/remove an item from selection (generally done by ApplyRequests() function) + IMGUI_API bool GetNextSelectedItem(void** opaque_it, ImGuiID* out_id); // Iterate selection with 'void* it = NULL; ImGuiId id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' + inline ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. +}; - // items_count: Use -1 to ignore (you can call Begin later). Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step). - // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). - // If you don't specify an items_height, you NEED to call Step(). If you specify items_height you may call the old Begin()/End() api directly, but prefer calling Step(). - ImGuiListClipper(int items_count = -1, float items_height = -1.0f) - { - Begin(items_count, items_height); - } // NB: Begin() initialize every fields (as we allow user to call Begin/End multiple times on a same instance if they want). - ~ImGuiListClipper() - { - IM_ASSERT(ItemsCount == -1); - } // Assert if user forgot to call End() or Step() until false. +// Optional helper to apply multi-selection requests to existing randomly accessible storage. +// Convenient if you want to quickly wire multi-select API on e.g. an array of bool or items storing their own selection state. +struct ImGuiSelectionExternalStorage +{ + // Members + void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } - IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. - IMGUI_API void Begin(int items_count, float items_height = -1.0f); // Automatically called by constructor if you passed 'items_count' or by Step() in Step 1. - IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + // Methods + IMGUI_API ImGuiSelectionExternalStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests by using AdapterSetItemSelected() calls }; //----------------------------------------------------------------------------- -// Draw List +// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. //----------------------------------------------------------------------------- -// Draw callbacks for advanced uses. -// NB- You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering (you can poke into the draw list for that) -// Draw callback may be useful for example, A) Change your GPU render state, B) render a complex 3D scene inside a UI element (without an intermediate texture/render target), etc. -// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) cmd.UserCallback(parent_list, cmd); else RenderTriangles()' -typedef void (*ImDrawCallback)(const ImDrawList *parent_list, const ImDrawCmd *cmd); +// The maximum line width to bake anti-aliased textures for. Build atlas with ImFontAtlasFlags_NoBakedLines to disable baking. +#ifndef IM_DRAWLIST_TEX_LINES_WIDTH_MAX +#define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (63) +#endif + +// ImDrawCallback: Draw callbacks for advanced uses [configurable type: override in imconfig.h] +// NB: You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering, +// you can poke into the draw list for that! Draw callback may be useful for example to: +// A) Change your GPU render state, +// B) render a complex 3D scene inside a UI element without an intermediate texture/render target, etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) { cmd.UserCallback(parent_list, cmd); } else { RenderTriangles() }' +// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering backend accordingly. +#ifndef ImDrawCallback +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); +#endif + +// Special Draw callback value to request renderer backend to reset the graphics/render state. +// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. +// This is useful, for example, if you submitted callbacks which you know have altered the render state and you want it to be restored. +// Render state is not reset by default because they are many perfectly useful way of altering render state (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) // Typically, 1 command = 1 GPU draw call (unless command is a callback) +// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, +// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. +// Backends made for <1.71. will typically ignore the VtxOffset fields. +// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for). struct ImDrawCmd { - unsigned int ElemCount; // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. - ImVec4 ClipRect; // Clipping rectangle (x1, y1, x2, y2) - ImTextureID TextureId; // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. - ImDrawCallback UserCallback; // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. - void *UserCallbackData; // The draw callback code can access this. - - ImDrawCmd() - { - ElemCount = 0; - ClipRect.x = ClipRect.y = ClipRect.z = ClipRect.w = 0.0f; - TextureId = NULL; - UserCallback = NULL; - UserCallbackData = NULL; - } + ImVec4 ClipRect; // 4*4 // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates + ImTextureID TextureId; // 4-8 // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + unsigned int VtxOffset; // 4 // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices. + unsigned int IdxOffset; // 4 // Start offset in index buffer. + unsigned int ElemCount; // 4 // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImDrawCallback UserCallback; // 4-8 // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // 4-8 // The draw callback code can access this. + + ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed + + // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) + inline ImTextureID GetTexID() const { return TextureId; } }; -// Vertex index (override with '#define ImDrawIdx unsigned int' inside in imconfig.h) -#ifndef ImDrawIdx -typedef unsigned short ImDrawIdx; -#endif - // Vertex layout #ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT struct ImDrawVert { - ImVec2 pos; - ImVec2 uv; - ImU32 col; + ImVec2 pos; + ImVec2 uv; + ImU32 col; }; #else // You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h // The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. -// The type has to be described within the macro (you can either declare the struct or use a typedef) +// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared at the time you'd want to set your type up. // NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; #endif -// Draw channels are used by the Columns API to "split" the render list into different channels while building, so items of each column can be batched together. -// You can also use them to simulate drawing layers and submit primitives in a different order than how they will be rendered. +// [Internal] For use by ImDrawList +struct ImDrawCmdHeader +{ + ImVec4 ClipRect; + ImTextureID TextureId; + unsigned int VtxOffset; +}; + +// [Internal] For use by ImDrawListSplitter struct ImDrawChannel { - ImVector CmdBuffer; - ImVector IdxBuffer; + ImVector _CmdBuffer; + ImVector _IdxBuffer; +}; + + +// Split/Merge functions are used to split the draw list into different layers which can be drawn into out of order. +// This is used by the Columns/Tables API, so items of each column can be batched together in a same draw call. +struct ImDrawListSplitter +{ + int _Current; // Current channel number (0) + int _Count; // Number of active channels (1+) + ImVector _Channels; // Draw channels (not resized down so _Count might be < Channels.Size) + + inline ImDrawListSplitter() { memset(this, 0, sizeof(*this)); } + inline ~ImDrawListSplitter() { ClearFreeMemory(); } + inline void Clear() { _Current = 0; _Count = 1; } // Do not clear Channels[] so our allocations are reused next frame + IMGUI_API void ClearFreeMemory(); + IMGUI_API void Split(ImDrawList* draw_list, int count); + IMGUI_API void Merge(ImDrawList* draw_list); + IMGUI_API void SetCurrentChannel(ImDrawList* draw_list, int channel_idx); }; -enum ImDrawCornerFlags_ +// Flags for ImDrawList functions +// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused) +enum ImDrawFlags_ { - ImDrawCornerFlags_TopLeft = 1 << 0, // 0x1 - ImDrawCornerFlags_TopRight = 1 << 1, // 0x2 - ImDrawCornerFlags_BotLeft = 1 << 2, // 0x4 - ImDrawCornerFlags_BotRight = 1 << 3, // 0x8 - ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, // 0x3 - ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, // 0xC - ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, // 0x5 - ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, // 0xA - ImDrawCornerFlags_All = 0xF // In your function calls you may use ~0 (= all bits sets) instead of ImDrawCornerFlags_All, as a convenience + ImDrawFlags_None = 0, + ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason) + ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. + ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. + ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. + ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. + ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! + ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, + ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. + ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, }; +// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. +// It is however possible to temporarily alter flags between calls to ImDrawList:: functions. enum ImDrawListFlags_ { - ImDrawListFlags_AntiAliasedLines = 1 << 0, - ImDrawListFlags_AntiAliasedFill = 1 << 1 + ImDrawListFlags_None = 0, + ImDrawListFlags_AntiAliasedLines = 1 << 0, // Enable anti-aliased lines/borders (*2 the number of triangles for 1.0f wide line or lines thin enough to be drawn using textures, otherwise *3 the number of triangles) + ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). + ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. }; // Draw command list -// This is the low-level list of polygons that ImGui functions are filling. At the end of the frame, all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. -// Each ImGui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to access the current window draw list and draw custom primitives. +// This is the low-level list of polygons that ImGui:: functions are filling. At the end of the frame, +// all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each dear imgui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to +// access the current window draw list and draw custom primitives. // You can interleave normal ImGui:: calls and adding primitives to the current draw list. -// All positions are generally in pixel coordinates (top-left at (0,0), bottom-right at io.DisplaySize), however you are totally free to apply whatever transformation matrix to want to the data (if you apply such transformation you'll want to apply it to ClipRect as well) +// In single viewport mode, top-left is == GetMainViewport()->Pos (generally 0,0), bottom-right is == GetMainViewport()->Pos+Size (generally io.DisplaySize). +// You are totally free to apply whatever transformation matrix to want to the data (depending on the use of the transformation you may want to apply it to ClipRect as well!) // Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. struct ImDrawList { - // This is what you have to render - ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. - ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those - ImVector VtxBuffer; // Vertex buffer. - - // [Internal, used while building lists] - ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. - const ImDrawListSharedData *_Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) - const char *_OwnerName; // Pointer to owner window's name for debugging - unsigned int _VtxCurrentIdx; // [Internal] == VtxBuffer.Size - ImDrawVert *_VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) - ImDrawIdx *_IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) - ImVector _ClipRectStack; // [Internal] - ImVector _TextureIdStack; // [Internal] - ImVector _Path; // [Internal] current path building - int _ChannelsCurrent; // [Internal] current channel number (0) - int _ChannelsCount; // [Internal] number of active channels (1+) - ImVector _Channels; // [Internal] draw channels for columns API (not resized down so _ChannelsCount may be smaller than _Channels.Size) - - // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) - ImDrawList(const ImDrawListSharedData *shared_data) - { - _Data = shared_data; - _OwnerName = NULL; - Clear(); - } - ~ImDrawList() - { - ClearFreeMemory(); - } - IMGUI_API void PushClipRect(ImVec2 clip_rect_min, ImVec2 clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) - IMGUI_API void PushClipRectFullScreen(); - IMGUI_API void PopClipRect(); - IMGUI_API void PushTextureID(ImTextureID texture_id); - IMGUI_API void PopTextureID(); - inline ImVec2 GetClipRectMin() const - { - const ImVec4 &cr = _ClipRectStack.back(); - return ImVec2(cr.x, cr.y); - } - inline ImVec2 GetClipRectMax() const - { - const ImVec4 &cr = _ClipRectStack.back(); - return ImVec2(cr.z, cr.w); - } - - // Primitives - IMGUI_API void AddLine(const ImVec2 &a, const ImVec2 &b, ImU32 col, float thickness = 1.0f); - IMGUI_API void AddRect(const ImVec2 &a, const ImVec2 &b, ImU32 col, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All, float thickness = 1.0f); // a: upper-left, b: lower-right, rounding_corners_flags: 4-bits corresponding to which corner to round - IMGUI_API void AddRectFilled(const ImVec2 &a, const ImVec2 &b, ImU32 col, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All); // a: upper-left, b: lower-right - IMGUI_API void AddRectFilledMultiColor(const ImVec2 &a, const ImVec2 &b, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); - IMGUI_API void AddQuad(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col, float thickness = 1.0f); - IMGUI_API void AddQuadFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col); - IMGUI_API void AddTriangle(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col, float thickness = 1.0f); - IMGUI_API void AddTriangleFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col); - IMGUI_API void AddCircle(const ImVec2 ¢re, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); - IMGUI_API void AddCircleFilled(const ImVec2 ¢re, float radius, ImU32 col, int num_segments = 12); - IMGUI_API void AddText(const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end = NULL); - IMGUI_API void AddText(const ImFont *font, float font_size, const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end = NULL, float wrap_width = 0.0f, const ImVec4 *cpu_fine_clip_rect = NULL); - IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a = ImVec2(0, 0), const ImVec2 &uv_b = ImVec2(1, 1), ImU32 col = 0xFFFFFFFF); - IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, const ImVec2 &uv_a = ImVec2(0, 0), const ImVec2 &uv_b = ImVec2(1, 0), const ImVec2 &uv_c = ImVec2(1, 1), const ImVec2 &uv_d = ImVec2(0, 1), ImU32 col = 0xFFFFFFFF); - IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, ImU32 col, float rounding, int rounding_corners = ImDrawCornerFlags_All); - IMGUI_API void AddPolyline(const ImVec2 *points, const int num_points, ImU32 col, bool closed, float thickness); - IMGUI_API void AddConvexPolyFilled(const ImVec2 *points, const int num_points, ImU32 col); - IMGUI_API void AddBezierCurve(const ImVec2 &pos0, const ImVec2 &cp0, const ImVec2 &cp1, const ImVec2 &pos1, ImU32 col, float thickness, int num_segments = 0); - - // Stateful path API, add points then finish with PathFill() or PathStroke() - inline void PathClear() - { - _Path.resize(0); - } - inline void PathLineTo(const ImVec2 &pos) - { - _Path.push_back(pos); - } - inline void PathLineToMergeDuplicate(const ImVec2 &pos) - { - if (_Path.Size == 0 || memcmp(&_Path[_Path.Size - 1], &pos, 8) != 0) - _Path.push_back(pos); - } - inline void PathFillConvex(ImU32 col) - { - AddConvexPolyFilled(_Path.Data, _Path.Size, col); - PathClear(); - } - inline void PathStroke(ImU32 col, bool closed, float thickness = 1.0f) - { - AddPolyline(_Path.Data, _Path.Size, col, closed, thickness); - PathClear(); - } - IMGUI_API void PathArcTo(const ImVec2 ¢re, float radius, float a_min, float a_max, int num_segments = 10); - IMGUI_API void PathArcToFast(const ImVec2 ¢re, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle - IMGUI_API void PathBezierCurveTo(const ImVec2 &p1, const ImVec2 &p2, const ImVec2 &p3, int num_segments = 0); - IMGUI_API void PathRect(const ImVec2 &rect_min, const ImVec2 &rect_max, float rounding = 0.0f, int rounding_corners_flags = ImDrawCornerFlags_All); - - // Channels - // - Use to simulate layers. By switching channels to can render out-of-order (e.g. submit foreground primitives before background primitives) - // - Use to minimize draw calls (e.g. if going back-and-forth between multiple non-overlapping clipping rectangles, prefer to append into separate channels then merge at the end) - IMGUI_API void ChannelsSplit(int channels_count); - IMGUI_API void ChannelsMerge(); - IMGUI_API void ChannelsSetCurrent(int channel_index); - - // Advanced - IMGUI_API void AddCallback(ImDrawCallback callback, void *callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. - IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible - - // Internal helpers - // NB: all primitives needs to be reserved via PrimReserve() beforehand! - IMGUI_API void Clear(); - IMGUI_API void ClearFreeMemory(); - IMGUI_API void PrimReserve(int idx_count, int vtx_count); - IMGUI_API void PrimRect(const ImVec2 &a, const ImVec2 &b, ImU32 col); // Axis aligned rectangle (composed of two triangles) - IMGUI_API void PrimRectUV(const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, ImU32 col); - IMGUI_API void PrimQuadUV(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, const ImVec2 &uv_a, const ImVec2 &uv_b, const ImVec2 &uv_c, const ImVec2 &uv_d, ImU32 col); - inline void PrimWriteVtx(const ImVec2 &pos, const ImVec2 &uv, ImU32 col) - { - _VtxWritePtr->pos = pos; - _VtxWritePtr->uv = uv; - _VtxWritePtr->col = col; - _VtxWritePtr++; - _VtxCurrentIdx++; - } - inline void PrimWriteIdx(ImDrawIdx idx) - { - *_IdxWritePtr = idx; - _IdxWritePtr++; - } - inline void PrimVtx(const ImVec2 &pos, const ImVec2 &uv, ImU32 col) - { - PrimWriteIdx((ImDrawIdx) _VtxCurrentIdx); - PrimWriteVtx(pos, uv, col); - } - IMGUI_API void UpdateClipRect(); - IMGUI_API void UpdateTextureID(); -}; - -// All draw data to render an ImGui frame + // This is what you have to render + ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. + ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those + ImVector VtxBuffer; // Vertex buffer. + ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. + + // [Internal, used while building lists] + unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) + ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _Path; // [Internal] current path building + ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). + ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] + float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + const char* _OwnerName; // Pointer to owner window's name for debugging + + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) + ImDrawList(ImDrawListSharedData* shared_data) { memset(this, 0, sizeof(*this)); _Data = shared_data; } + + ~ImDrawList() { _ClearFreeMemory(); } + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + IMGUI_API void PushClipRectFullScreen(); + IMGUI_API void PopClipRect(); + IMGUI_API void PushTextureID(ImTextureID texture_id); + IMGUI_API void PopTextureID(); + inline ImVec2 GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); } + inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } + + // Primitives + // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In older versions (until Dear ImGui 1.77) the AddCircle functions defaulted to num_segments == 12. + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides. + IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); + IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0); + IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); + IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + + // General polygon + // - Only simple polygons are supported by filling functions (no self-intersections, no holes). + // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience fo user but not used by main library. + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); + + // Image primitives + // - Read FAQ to understand what ImTextureID is. + // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle. + // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture. + IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); + + // Stateful path API, add points then finish with PathFillConvex() or PathStroke() + // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // so e.g. 'PathArcTo(center, radius, PI * -0.5f, PI)' is ok, whereas 'PathArcTo(center, radius, PI, PI * -0.5f)' won't have correct anti-aliasing when followed by PathFillConvex(). + inline void PathClear() { _Path.Size = 0; } + inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } + IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); + IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse + IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); + + // Advanced + IMGUI_API void AddCallback(ImDrawCallback callback, void* callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. + IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible + IMGUI_API ImDrawList* CloneOutput() const; // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. + + // Advanced: Channels + // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) + // - This API shouldn't have been in ImDrawList in the first place! + // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. + // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. + inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } + inline void ChannelsMerge() { _Splitter.Merge(this); } + inline void ChannelsSetCurrent(int n) { _Splitter.SetCurrentChannel(this, n); } + + // Advanced: Primitives allocations + // - We render triangles (three vertices) + // - All primitives needs to be reserved via PrimReserve() beforehand. + IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimUnreserve(int idx_count, int vtx_count); + IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) + IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index + + // Obsolete names + //inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0) { PathEllipticalArcTo(center, ImVec2(radius_x, radius_y), rot, a_min, a_max, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + //inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + + // [Internal helpers] + IMGUI_API void _ResetForNewFrame(); + IMGUI_API void _ClearFreeMemory(); + IMGUI_API void _PopUnusedDrawCmd(); + IMGUI_API void _TryMergeDrawCmds(); + IMGUI_API void _OnChangedClipRect(); + IMGUI_API void _OnChangedTextureID(); + IMGUI_API void _OnChangedVtxOffset(); + IMGUI_API void _SetTextureID(ImTextureID texture_id); + IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; + IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); + IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); +}; + +// All draw data to render a Dear ImGui frame +// (NB: the style and the naming convention here is a little inconsistent, we currently preserve them for backward compatibility purpose, +// as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) struct ImDrawData { - bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. - ImDrawList **CmdLists; - int CmdListsCount; - int TotalVtxCount; // For convenience, sum of all cmd_lists vtx_buffer.Size - int TotalIdxCount; // For convenience, sum of all cmd_lists idx_buffer.Size - - // Functions - ImDrawData() - { - Clear(); - } - void Clear() - { - Valid = false; - CmdLists = NULL; - CmdListsCount = TotalVtxCount = TotalIdxCount = 0; - } // Draw lists are owned by the ImGuiContext and only pointed to here. - IMGUI_API void DeIndexAllBuffers(); // For backward compatibility or convenience: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! - IMGUI_API void ScaleClipRects(const ImVec2 &sc); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render (should always be == CmdLists.size) + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImVector CmdLists; // Array of ImDrawList* to render. The ImDrawLists are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + ImGuiViewport* OwnerViewport; // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not). + + // Functions + ImDrawData() { Clear(); } + IMGUI_API void Clear(); + IMGUI_API void AddDrawList(ImDrawList* draw_list); // Helper to add an external draw list into an existing ImDrawData. + IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! + IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. }; +//----------------------------------------------------------------------------- +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) +//----------------------------------------------------------------------------- + struct ImFontConfig { - void *FontData; // // TTF/OTF data - int FontDataSize; // // TTF/OTF data size - bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). - int FontNo; // 0 // Index of font within TTF/OTF file - float SizePixels; // // Size in pixels for rasterizer. - int OversampleH, OversampleV; // 3, 1 // Rasterize at higher quality for sub-pixel positioning. We don't use sub-pixel positions on the Y axis. - bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. - ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. - ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. - const ImWchar *GlyphRanges; // NULL // Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. - bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. - unsigned int RasterizerFlags; // 0x00 // Settings for custom font rasterizer (e.g. ImGuiFreeType). Leave as zero if you aren't using one. - float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. + void* FontData; // // TTF/OTF data + int FontDataSize; // // TTF/OTF data size + bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + int FontNo; // 0 // Index of font within TTF/OTF file + float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). + int OversampleH; // 2 // Rasterize at higher quality for sub-pixel positioning. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleV; // 1 // Rasterize at higher quality for sub-pixel positioning. This is not really useful as we don't use sub-pixel positions on the Y axis. + bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. + ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. + ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. + const ImWchar* GlyphRanges; // NULL // THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). + float GlyphMinAdvanceX; // 0 // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font + float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. + float RasterizerMultiply; // 1.0f // Linearly brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. This is a silly thing we may remove in the future. + float RasterizerDensity; // 1.0f // DPI scale for rasterization, not altering other font metrics: make it easy to swap between e.g. a 100% and a 400% fonts for a zooming display. IMPORTANT: If you increase this it is expected that you increase font scale accordingly, otherwise quality may look lowered. + ImWchar EllipsisChar; // -1 // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. + + // [Internal] + char Name[40]; // Name (strictly to ease debugging) + ImFont* DstFont; + + IMGUI_API ImFontConfig(); +}; - // [Internal] - char Name[32]; // Name (strictly to ease debugging) - ImFont *DstFont; +// Hold rendering data for one glyph. +// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this) +struct ImFontGlyph +{ + unsigned int Colored : 1; // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops) + unsigned int Visible : 1; // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering. + unsigned int Codepoint : 30; // 0x0000..0x10FFFF + float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) + float X0, Y0, X1, Y1; // Glyph corners + float U0, V0, U1, V1; // Texture coordinates +}; - IMGUI_API ImFontConfig(); +// Helper to build glyph ranges from text/string data. Feed your application strings/characters to it then call BuildRanges(). +// This is essentially a tightly packed of vector of 64k booleans = 8KB storage. +struct ImFontGlyphRangesBuilder +{ + ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) + + ImFontGlyphRangesBuilder() { Clear(); } + inline void Clear() { int size_in_bytes = (IM_UNICODE_CODEPOINT_MAX + 1) / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } + inline bool GetBit(size_t n) const { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array + inline void SetBit(size_t n) { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array + inline void AddChar(ImWchar c) { SetBit(c); } // Add character + IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) + IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault()) to force add all of ASCII/Latin+Ext + IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges }; -struct ImFontGlyph +// See ImFontAtlas::AddCustomRectXXX functions. +struct ImFontAtlasCustomRect { - ImWchar Codepoint; // 0x0000..0xFFFF - float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) - float X0, Y0, X1, Y1; // Glyph corners - float U0, V0, U1, V1; // Texture coordinates + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned short X, Y; // Output // Packed position in Atlas + unsigned int GlyphID; // Input // For custom font glyphs only (ID < 0x110000) + float GlyphAdvanceX; // Input // For custom font glyphs only: glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only: glyph display offset + ImFont* Font; // Input // For custom font glyphs only: target font + ImFontAtlasCustomRect() { Width = Height = 0; X = Y = 0xFFFF; GlyphID = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; } + bool IsPacked() const { return X != 0xFFFF; } }; +// Flags for ImFontAtlas build enum ImFontAtlasFlags_ { - ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two - ImFontAtlasFlags_NoMouseCursors = 1 << 1 // Don't build software mouse cursors into the atlas + ImFontAtlasFlags_None = 0, + ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two + ImFontAtlasFlags_NoMouseCursors = 1 << 1, // Don't build software mouse cursors into the atlas (save a little texture memory) + ImFontAtlasFlags_NoBakedLines = 1 << 2, // Don't build thick line textures into the atlas (save a little texture memory, allow support for point/nearest filtering). The AntiAliasedLinesUseTex features uses them, otherwise they will be rendered using polygons (more expensive for CPU/GPU). }; -// Load and rasterize multiple TTF/OTF fonts into a same texture. -// Sharing a texture for multiple fonts allows us to reduce the number of draw calls during rendering. -// We also add custom graphic data into the texture that serves for ImGui. -// 1. (Optional) Call AddFont*** functions. If you don't call any, the default font will be loaded for you. -// 2. Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. -// 3. Upload the pixels data into a texture within your graphics system. -// 4. Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture. This value will be passed back to you during rendering to identify the texture. -// IMPORTANT: If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the ImFont is build (when calling GetTextData*** or Build()). We only copy the pointer, not the data. +// Load and rasterize multiple TTF/OTF fonts into a same texture. The font atlas will build a single texture holding: +// - One or more fonts. +// - Custom graphics data needed to render the shapes needed by Dear ImGui. +// - Mouse cursor shapes for software cursor rendering (unless setting 'Flags |= ImFontAtlasFlags_NoMouseCursors' in the font atlas). +// It is the user-code responsibility to setup/build the atlas, then upload the pixel data into a texture accessible by your graphics api. +// - Optionally, call any of the AddFont*** functions. If you don't call any, the default font embedded in the code will be loaded for you. +// - Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// - Upload the pixels data into a texture within your graphics system (see imgui_impl_xxxx.cpp examples) +// - Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture in a format natural to your graphics API. +// This value will be passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID for more details. +// Common pitfalls: +// - If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the +// atlas is build (when calling GetTexData*** or Build()). We only copy the pointer, not the data. +// - Important: By default, AddFontFromMemoryTTF() takes ownership of the data. Even though we are not writing to it, we will free the pointer on destruction. +// You can set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed, +// - Even though many functions are suffixed with "TTF", OTF data is supported just as well. +// - This is an old API and it is currently awkward for those and various other reasons! We will address them in the future! struct ImFontAtlas { - IMGUI_API ImFontAtlas(); - IMGUI_API ~ImFontAtlas(); - IMGUI_API ImFont *AddFont(const ImFontConfig *font_cfg); - IMGUI_API ImFont *AddFontDefault(const ImFontConfig *font_cfg = NULL); - IMGUI_API ImFont *AddFontFromFileTTF(const char *filename, float size_pixels, const ImFontConfig *font_cfg = NULL, const ImWchar *glyph_ranges = NULL); - IMGUI_API ImFont *AddFontFromMemoryTTF(void *font_data, int font_size, float size_pixels, const ImFontConfig *font_cfg = NULL, const ImWchar *glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after Build(). Set font_cfg->FontDataOwnedByAtlas to false to keep ownership. - IMGUI_API ImFont *AddFontFromMemoryCompressedTTF(const void *compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig *font_cfg = NULL, const ImWchar *glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. - IMGUI_API ImFont *AddFontFromMemoryCompressedBase85TTF(const char *compressed_font_data_base85, float size_pixels, const ImFontConfig *font_cfg = NULL, const ImWchar *glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. - IMGUI_API void ClearTexData(); // Clear the CPU-side texture data. Saves RAM once the texture has been copied to graphics memory. - IMGUI_API void ClearInputData(); // Clear the input TTF data (inc sizes, glyph ranges) - IMGUI_API void ClearFonts(); // Clear the ImGui-side font data (glyphs storage, UV coordinates) - IMGUI_API void Clear(); // Clear all - - // Build atlas, retrieve pixel data. - // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). - // RGBA32 format is provided for convenience and compatibility, but note that unless you use CustomRect to draw color data, the RGB pixels emitted from Fonts will all be white (~75% of waste). - // Pitch = Width * BytesPerPixels - IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. - IMGUI_API void GetTexDataAsAlpha8(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel = NULL); // 1 byte per-pixel - IMGUI_API void GetTexDataAsRGBA32(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel = NULL); // 4 bytes-per-pixel - void SetTexID(ImTextureID id) - { - TexID = id; - } - - //------------------------------------------- - // Glyph Ranges - //------------------------------------------- - - // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) - // NB: Make sure that your string are UTF-8 and NOT in your local code page. In C++11, you can create UTF-8 string literal using the u8"Hello world" syntax. See FAQ for details. - IMGUI_API const ImWchar *GetGlyphRangesDefault(); // Basic Latin, Extended Latin - IMGUI_API const ImWchar *GetGlyphRangesKorean(); // Default + Korean characters - IMGUI_API const ImWchar *GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs - IMGUI_API const ImWchar *GetGlyphRangesChinese(); // Default + Japanese + full set of about 21000 CJK Unified Ideographs - IMGUI_API const ImWchar *GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters - IMGUI_API const ImWchar *GetGlyphRangesThai(); // Default + Thai characters - - // Helpers to build glyph ranges from text data. Feed your application strings/characters to it then call BuildRanges(). - struct GlyphRangesBuilder - { - ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) - GlyphRangesBuilder() - { - UsedChars.resize(0x10000 / 8); - memset(UsedChars.Data, 0, 0x10000 / 8); - } - bool GetBit(int n) - { - return (UsedChars[n >> 3] & (1 << (n & 7))) != 0; - } - void SetBit(int n) - { - UsedChars[n >> 3] |= 1 << (n & 7); - } // Set bit 'c' in the array - void AddChar(ImWchar c) - { - SetBit(c); - } // Add character - IMGUI_API void AddText(const char *text, const char *text_end = NULL); // Add string (each character of the UTF-8 string are added) - IMGUI_API void AddRanges(const ImWchar *ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault) to force add all of ASCII/Latin+Ext - IMGUI_API void BuildRanges(ImVector *out_ranges); // Output new ranges - }; - - //------------------------------------------- - // Custom Rectangles/Glyphs API - //------------------------------------------- - - // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. After calling Build(), you can query the rectangle position and render your pixels. - // You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), so you can render e.g. custom colorful icons and use them as regular glyphs. - struct CustomRect - { - unsigned int ID; // Input // User ID. Use <0x10000 to map into a font glyph, >=0x10000 for other/internal/custom texture data. - unsigned short Width, Height; // Input // Desired rectangle dimension - unsigned short X, Y; // Output // Packed position in Atlas - float GlyphAdvanceX; // Input // For custom font glyphs only (ID<0x10000): glyph xadvance - ImVec2 GlyphOffset; // Input // For custom font glyphs only (ID<0x10000): glyph display offset - ImFont *Font; // Input // For custom font glyphs only (ID<0x10000): target font - CustomRect() - { - ID = 0xFFFFFFFF; - Width = Height = 0; - X = Y = 0xFFFF; - GlyphAdvanceX = 0.0f; - GlyphOffset = ImVec2(0, 0); - Font = NULL; - } - bool IsPacked() const - { - return X != 0xFFFF; - } - }; - - IMGUI_API int AddCustomRectRegular(unsigned int id, int width, int height); // Id needs to be >= 0x10000. Id >= 0x80000000 are reserved for ImGui and ImDrawList - IMGUI_API int AddCustomRectFontGlyph(ImFont *font, ImWchar id, int width, int height, float advance_x, const ImVec2 &offset = ImVec2(0, 0)); // Id needs to be < 0x10000 to register a rectangle to map into a specific font. - const CustomRect *GetCustomRectByIndex(int index) const - { - if (index < 0) - return NULL; - return &CustomRects[index]; - } - - // Internals - IMGUI_API void CalcCustomRectUV(const CustomRect *rect, ImVec2 *out_uv_min, ImVec2 *out_uv_max); - IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2 *out_offset, ImVec2 *out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); - - //------------------------------------------- - // Members - //------------------------------------------- - - ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) - ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. - int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. - int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. - - // [Internal] - // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. - unsigned char *TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight - unsigned int *TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 - int TexWidth; // Texture width calculated during Build(). - int TexHeight; // Texture height calculated during Build(). - ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) - ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel - ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. - ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. - ImVector ConfigData; // Internal data - int CustomRectIds[1]; // Identifiers of custom texture rectangle used by ImFontAtlas/ImDrawList + IMGUI_API ImFontAtlas(); + IMGUI_API ~ImFontAtlas(); + IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); + IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); + IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. + IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. + IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. + IMGUI_API void ClearFonts(); // Clear output font data (glyphs storage, UV coordinates). + IMGUI_API void Clear(); // Clear all input and output. + + // Build atlas, retrieve pixel data. + // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). + // The pitch is always = Width * BytesPerPixels (1 or 4) + // Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into + // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste. + IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. + IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel + IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel + bool IsBuilt() const { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent... + void SetTexID(ImTextureID id) { TexID = id; } + + //------------------------------------------- + // Glyph Ranges + //------------------------------------------- + + // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) + // NB: Make sure that your string are UTF-8 and NOT in your local code page. + // Read https://github.com/ocornut/imgui/blob/master/docs/FONTS.md/#about-utf-8-encoding for details. + // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data. + IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesGreek(); // Default + Greek and Coptic + IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseFull(); // Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseSimplifiedCommon();// Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese + IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters + IMGUI_API const ImWchar* GetGlyphRangesThai(); // Default + Thai characters + IMGUI_API const ImWchar* GetGlyphRangesVietnamese(); // Default + Vietnamese characters + + //------------------------------------------- + // [BETA] Custom Rectangles/Glyphs API + //------------------------------------------- + + // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. + // - After calling Build(), you can query the rectangle position and render your pixels. + // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of preferred texture format. + // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), + // so you can render e.g. custom colorful icons and use them as regular glyphs. + // - Read docs/FONTS.md for more details about using colorful icons. + // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings. + IMGUI_API int AddCustomRectRegular(int width, int height); + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0)); + ImFontAtlasCustomRect* GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; } + + // [Internal] + IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const; + IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + + //------------------------------------------- + // Members + //------------------------------------------- + + ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) + ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. + int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. + int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0 (will also need to set AntiAliasedLinesUseTex = false). + bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + void* UserData; // Store your own atlas related user-data (if e.g. you have multiple font atlas). + + // [Internal] + // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + bool TexReady; // Set when texture was built matching current font input + bool TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format. + unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight + unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 + int TexWidth; // Texture width calculated during Build(). + int TexHeight; // Texture height calculated during Build(). + ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) + ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel + ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. + ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. + ImVector ConfigData; // Configuration data + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + + // [Internal] Font builder + const ImFontBuilderIO* FontBuilderIO; // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE). + unsigned int FontBuilderFlags; // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig. + + // [Internal] Packing data + int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors + int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines + + // [Obsolete] + //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ + //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ }; // Font runtime data and rendering // ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). struct ImFont { - // Members: Hot ~62/78 bytes - float FontSize; // // Height of characters, set during loading (don't change after loading) - float Scale; // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetFontScale() - ImVec2 DisplayOffset; // = (0.f,1.f) // Offset font rendering by xx pixels - ImVector Glyphs; // // All glyphs. - ImVector IndexAdvanceX; // // Sparse. Glyphs->AdvanceX in a directly indexable way (more cache-friendly, for CalcTextSize functions which are often bottleneck in large UI). - ImVector IndexLookup; // // Sparse. Index glyphs by Unicode code-point. - const ImFontGlyph *FallbackGlyph; // == FindGlyph(FontFallbackChar) - float FallbackAdvanceX; // == FallbackGlyph->AdvanceX - ImWchar FallbackChar; // = '?' // Replacement glyph if one isn't found. Only set via SetFallbackChar() - - // Members: Cold ~18/26 bytes - short ConfigDataCount; // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. - ImFontConfig *ConfigData; // // Pointer within ContainerAtlas->ConfigData - ImFontAtlas *ContainerAtlas; // // What we has been loaded into - float Ascent, Descent; // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] - int MetricsTotalSurface; // // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) - - // Methods - IMGUI_API ImFont(); - IMGUI_API ~ImFont(); - IMGUI_API void ClearOutputData(); - IMGUI_API void BuildLookupTable(); - IMGUI_API const ImFontGlyph *FindGlyph(ImWchar c) const; - IMGUI_API void SetFallbackChar(ImWchar c); - float GetCharAdvance(ImWchar c) const - { - return ((int) c < IndexAdvanceX.Size) ? IndexAdvanceX[(int) c] : FallbackAdvanceX; - } - bool IsLoaded() const - { - return ContainerAtlas != NULL; - } - const char *GetDebugName() const - { - return ConfigData ? ConfigData->Name : ""; - } - - // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. - // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. - IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char *text_begin, const char *text_end = NULL, const char **remaining = NULL) const; // utf8 - IMGUI_API const char *CalcWordWrapPositionA(float scale, const char *text, const char *text_end, float wrap_width) const; - IMGUI_API void RenderChar(ImDrawList *draw_list, float size, ImVec2 pos, ImU32 col, unsigned short c) const; - IMGUI_API void RenderText(ImDrawList *draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4 &clip_rect, const char *text_begin, const char *text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; - - // [Internal] - IMGUI_API void GrowIndex(int new_size); - IMGUI_API void AddGlyph(ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); - IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + // Members: Hot ~20/24 bytes (for CalcTextSize) + ImVector IndexAdvanceX; // 12-16 // out // // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this info, and are often bottleneck in large UI). + float FallbackAdvanceX; // 4 // out // = FallbackGlyph->AdvanceX + float FontSize; // 4 // in // // Height of characters/line, set during loading (don't change after loading) + + // Members: Hot ~28/40 bytes (for CalcTextSize + render loop) + ImVector IndexLookup; // 12-16 // out // // Sparse. Index glyphs by Unicode code-point. + ImVector Glyphs; // 12-16 // out // // All glyphs. + const ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) + + // Members: Cold ~32/40 bytes + ImFontAtlas* ContainerAtlas; // 4-8 // out // // What we has been loaded into + const ImFontConfig* ConfigData; // 4-8 // in // // Pointer within ContainerAtlas->ConfigData + short ConfigDataCount; // 2 // in // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. + ImWchar FallbackChar; // 2 // out // = FFFD/'?' // Character used if a glyph isn't found. + ImWchar EllipsisChar; // 2 // out // = '...'/'.'// Character used for ellipsis rendering. + short EllipsisCharCount; // 1 // out // 1 or 3 + float EllipsisWidth; // 4 // out // Width + float EllipsisCharStep; // 4 // out // Step between characters when EllipsisCount > 0 + bool DirtyLookupTables; // 1 // out // + float Scale; // 4 // in // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetWindowFontScale() + float Ascent, Descent; // 4+4 // out // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] (unscaled) + int MetricsTotalSurface;// 4 // out // // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + ImU8 Used4kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/4096/8]; // 2 bytes if ImWchar=ImWchar16, 34 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints. + + // Methods + IMGUI_API ImFont(); + IMGUI_API ~ImFont(); + IMGUI_API const ImFontGlyph*FindGlyph(ImWchar c) const; + IMGUI_API const ImFontGlyph*FindGlyphNoFallback(ImWchar c) const; + float GetCharAdvance(ImWchar c) const { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return ConfigData ? ConfigData->Name : ""; } + + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL) const; // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const; + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) const; + IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; + + // [Internal] Don't use! + IMGUI_API void BuildLookupTable(); + IMGUI_API void ClearOutputData(); + IMGUI_API void GrowIndex(int new_size); + IMGUI_API void AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + IMGUI_API void SetGlyphVisible(ImWchar c, bool visible); + IMGUI_API bool IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Viewports +//----------------------------------------------------------------------------- + +// Flags stored in ImGuiViewport::Flags, giving indications to the platform backends. +enum ImGuiViewportFlags_ +{ + ImGuiViewportFlags_None = 0, + ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window + ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) + ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: Is created/managed by the application (rather than a dear imgui backend) +}; + +// - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. +// - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. +// - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. +// - About Main Area vs Work Area: +// - Main Area = entire viewport. +// - Work Area = entire viewport minus sections used by main menu bars (for platform windows), or by task bar (for platform monitor). +// - Windows are generally trying to stay within the Work Area of their host viewport. +struct ImGuiViewport +{ + ImGuiID ID; // Unique identifier for the viewport + ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ + ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) + ImVec2 Size; // Main Area: Size of the viewport. + ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) + ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + + // Platform/Backend Dependent Data + void* PlatformHandle; // void* to hold higher-level, platform window handle (e.g. HWND, GLFWWindow*, SDL_Window*) + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms) + + ImGuiViewport() { memset(this, 0, sizeof(*this)); } + + // Helpers + ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } + ImVec2 GetWorkCenter() const { return ImVec2(WorkPos.x + WorkSize.x * 0.5f, WorkPos.y + WorkSize.y * 0.5f); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Platform Dependent Interfaces +//----------------------------------------------------------------------------- + +// Access via ImGui::GetPlatformIO() +struct ImGuiPlatformIO +{ + IMGUI_API ImGuiPlatformIO(); + + //------------------------------------------------------------------ + // Input - Interface with OS/backends + //------------------------------------------------------------------ + + // Optional: Access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*Platform_GetClipboardTextFn)(ImGuiContext* ctx); + void (*Platform_SetClipboardTextFn)(ImGuiContext* ctx, const char* text); + void* Platform_ClipboardUserData; + + // Optional: Open link/folder/file in OS Shell + // (default to use ShellExecuteA() on Windows, system() on Linux/Mac) + bool (*Platform_OpenInShellFn)(ImGuiContext* ctx, const char* path); + void* Platform_OpenInShellUserData; + + // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) + // (default to use native imm32 api on Windows) + void (*Platform_SetImeDataFn)(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + void* Platform_ImeUserData; + //void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); // [Renamed to platform_io.PlatformSetImeDataFn in 1.91.1] + + // Optional: Platform locale + // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point + ImWchar Platform_LocaleDecimalPoint; // '.' +}; + +// (Optional) Support for IME (Input Method Editor) via the platform_io.Platform_SetImeDataFn() function. +struct ImGuiPlatformImeData +{ + bool WantVisible; // A widget wants the IME to be visible + ImVec2 InputPos; // Position of the input cursor + float InputLineHeight; // Line height + + ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Obsolete functions and types +// (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +//----------------------------------------------------------------------------- #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - typedef ImFontGlyph Glyph; // OBSOLETE 1.52+ +namespace ImGui +{ + // OBSOLETED in 1.91.0 (from July 2024) + static inline void PushButtonRepeat(bool repeat) { PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); } + static inline void PopButtonRepeat() { PopItemFlag(); } + static inline void PushTabStop(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopTabStop() { PopItemFlag(); } + IMGUI_API ImVec2 GetContentRegionMax(); // Content boundaries max (e.g. window boundaries including scrolling, or current column boundaries). You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMin(); // Content boundaries min for the window (roughly (0,0)-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMax(); // Content boundaries max for the window (roughly (0,0)+Size-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + // OBSOLETED in 1.90.0 (from September 2023) + static inline bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0) { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); } + static inline void EndChildFrame() { EndChild(); } + //static inline bool BeginChild(const char* str_id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + //static inline bool BeginChild(ImGuiID id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags) { return BeginChild(id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + static inline void ShowStackToolWindow(bool* p_open = NULL) { ShowIDStackToolWindow(p_open); } + IMGUI_API bool Combo(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int height_in_items = -1); + // OBSOLETED in 1.89.7 (from June 2023) + IMGUI_API void SetItemAllowOverlap(); // Use SetNextItemAllowOverlap() before item. + // OBSOLETED in 1.89.4 (from March 2023) + static inline void PushAllowKeyboardFocus(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopAllowKeyboardFocus() { PopItemFlag(); } + // OBSOLETED in 1.87 (from February 2022 but more formally obsoleted April 2024) + IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); // Map ImGuiKey_* values into legacy native key index. == io.KeyMap[key]. When using a 1.87+ backend using io.AddKeyEvent(), calling GetKeyIndex() with ANY ImGuiKey_XXXX values will return the same value! + //static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); return key; } + + // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //-- OBSOLETED in 1.89 (from August 2022) + //IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // --> Use new ImageButton() signature (explicit item id, regular FramePadding). Refer to code in 1.91 if you want to grab a copy of this version. + //-- OBSOLETED in 1.88 (from May 2022) + //static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. + //static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. + //-- OBSOLETED in 1.86 (from November 2021) + //IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Code removed, see 1.90 for last version of the code. Calculate range of visible items for large list of evenly sized items. Prefer using ImGuiListClipper. + //-- OBSOLETED in 1.85 (from August 2021) + //static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } + //-- OBSOLETED in 1.81 (from February 2021) + //static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } + //static inline bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1) { float height = GetTextLineHeightWithSpacing() * ((height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f) + GetStyle().FramePadding.y * 2.0f; return BeginListBox(label, ImVec2(0.0f, height)); } // Helper to calculate size from items_count and height_in_items + //static inline void ListBoxFooter() { EndListBox(); } + //-- OBSOLETED in 1.79 (from August 2020) + //static inline void OpenPopupContextItem(const char* str_id = NULL, ImGuiMouseButton mb = 1) { OpenPopupOnItemClick(str_id, mb); } // Bool return value removed. Use IsWindowAppearing() in BeginPopup() instead. Renamed in 1.77, renamed back in 1.79. Sorry! + //-- OBSOLETED in 1.78 (from June 2020): Old drag/sliders functions that took a 'float power > 1.0f' argument instead of ImGuiSliderFlags_Logarithmic. See github.com/ocornut/imgui/issues/3361 for details. + //IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f) // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //-- OBSOLETED in 1.77 and before + //static inline bool BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mb, bool over_items) { return BeginPopupContextWindow(str_id, mb | (over_items ? 0 : ImGuiPopupFlags_NoOpenOverItems)); } // OBSOLETED in 1.77 (from June 2020) + //static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } // OBSOLETED in 1.72 (from July 2019) + //static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } // OBSOLETED in 1.71 (from June 2019) + //static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } // OBSOLETED in 1.70 (from May 2019) + //static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } // OBSOLETED in 1.69 (from Mar 2019) + //static inline void SetScrollHere(float ratio = 0.5f) { SetScrollHereY(ratio); } // OBSOLETED in 1.66 (from Nov 2018) + //static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } // OBSOLETED in 1.63 (from Aug 2018) + //-- OBSOLETED in 1.60 and before + //static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } // OBSOLETED in 1.60 (from Apr 2018) + //static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) + //static inline void ShowTestWindow() { return ShowDemoWindow(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //IMGUI_API bool Begin(char* name, bool* p_open, ImVec2 size_first_use, float bg_alpha = -1.0f, ImGuiWindowFlags flags=0); // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017): Equivalent of using SetNextWindowSize(size, ImGuiCond_FirstUseEver) and SetNextWindowBgAlpha(). + //static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void SetNextWindowPosCenter(ImGuiCond c=0) { SetNextWindowPos(GetMainViewport()->GetCenter(), c, ImVec2(0.5f,0.5f)); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline bool IsItemHoveredRect() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsPosHoveringAnyWindow(const ImVec2&) { IM_ASSERT(0); return false; } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017): This was misleading and partly broken. You probably want to use the io.WantCaptureMouse flag instead. + //static inline bool IsMouseHoveringAnyWindow() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsMouseHoveringWindow() { return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //-- OBSOLETED in 1.50 and before + //static inline bool CollapsingHeader(char* label, const char* str_id, bool framed = true, bool default_open = false) { return CollapsingHeader(label, (default_open ? (1 << 5) : 0)); } // OBSOLETED in 1.49 + //static inline ImFont*GetWindowFont() { return GetFont(); } // OBSOLETED in 1.48 + //static inline float GetWindowFontSize() { return GetFontSize(); } // OBSOLETED in 1.48 + //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 +} + +//-- OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +//typedef ImDrawFlags ImDrawCornerFlags; +//enum ImDrawCornerFlags_ +//{ +// ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit +// ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). +// ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. +// ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. +// ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. +// ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 +// ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, +// ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, +// ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, +// ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, +//}; + +// RENAMED and MERGED both ImGuiKey_ModXXX and ImGuiModFlags_XXX into ImGuiMod_XXX (from September 2022) +// RENAMED ImGuiKeyModFlags -> ImGuiModFlags in 1.88 (from April 2022). Exceptionally commented out ahead of obscolescence schedule to reduce confusion and because they were not meant to be used in the first place. +//typedef ImGuiKeyChord ImGuiModFlags; // == int. We generally use ImGuiKeyChord to mean "a ImGuiKey or-ed with any number of ImGuiMod_XXX value", so you may store mods in there. +//enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiModFlags_Shift = ImGuiMod_Shift, ImGuiModFlags_Alt = ImGuiMod_Alt, ImGuiModFlags_Super = ImGuiMod_Super }; +//typedef ImGuiKeyChord ImGuiKeyModFlags; // == int +//enum ImGuiKeyModFlags_ { ImGuiKeyModFlags_None = 0, ImGuiKeyModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiKeyModFlags_Shift = ImGuiMod_Shift, ImGuiKeyModFlags_Alt = ImGuiMod_Alt, ImGuiKeyModFlags_Super = ImGuiMod_Super }; + +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // OBSOLETED IN 1.90 (now using C++11 standard version) + +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// RENAMED IMGUI_DISABLE_METRICS_WINDOW > IMGUI_DISABLE_DEBUG_TOOLS in 1.88 (from June 2022) +#if defined(IMGUI_DISABLE_METRICS_WINDOW) && !defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && !defined(IMGUI_DISABLE_DEBUG_TOOLS) +#define IMGUI_DISABLE_DEBUG_TOOLS +#endif +#if defined(IMGUI_DISABLE_METRICS_WINDOW) && defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) +#error IMGUI_DISABLE_METRICS_WINDOW was renamed to IMGUI_DISABLE_DEBUG_TOOLS, please use new name. #endif -}; + +//----------------------------------------------------------------------------- #if defined(__clang__) -# pragma clang diagnostic pop +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop #endif -// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +// Include imgui_user.h at the end of imgui.h +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. #ifdef IMGUI_INCLUDE_IMGUI_USER_H -# include "imgui_user.h" +#ifdef IMGUI_USER_H_FILENAME +#include IMGUI_USER_H_FILENAME +#else +#include "imgui_user.h" #endif +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/attachments/simple_engine/imgui/imgui_draw.cpp b/attachments/simple_engine/imgui/imgui_draw.cpp index 4015ce093..e033c357c 100644 --- a/attachments/simple_engine/imgui/imgui_draw.cpp +++ b/attachments/simple_engine/imgui/imgui_draw.cpp @@ -1,70 +1,90 @@ -// dear imgui, v1.60 WIP +// dear imgui, v1.91.1 // (drawing and font code) -// Contains implementation for -// - Default styles -// - ImDrawList -// - ImDrawData -// - ImFontAtlas -// - ImFont -// - Default font data +/* + +Index of this file: + +// [SECTION] STB libraries implementation +// [SECTION] Style functions +// [SECTION] ImDrawList +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +// [SECTION] ImDrawListSplitter +// [SECTION] ImDrawData +// [SECTION] Helpers ShadeVertsXXX functions +// [SECTION] ImFontConfig +// [SECTION] ImFontAtlas +// [SECTION] ImFontAtlas glyph ranges helpers +// [SECTION] ImFontGlyphRangesBuilder +// [SECTION] ImFont +// [SECTION] ImGui Internal Render Helpers +// [SECTION] Decompression code +// [SECTION] Default font data (ProggyClean.ttf) + +*/ #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -# define _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS #endif -#include "imgui.h" +#ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS -#include "imgui_internal.h" +#endif -#include // vsnprintf, sscanf, printf -#if !defined(alloca) -# ifdef _WIN32 -# include // alloca -# if !defined(alloca) -# define alloca _alloca // for clang with MS Codegen -# endif -# elif defined(__GLIBC__) || defined(__sun) -# include // alloca -# else -# include // alloca -# endif +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" +#ifdef IMGUI_ENABLE_FREETYPE +#include "misc/freetype/imgui_freetype.h" #endif +#include // vsnprintf, sscanf, printf + +// Visual Studio warnings #ifdef _MSC_VER -# pragma warning(disable : 4505) // unreferenced local function has been removed (stb stuff) -# pragma warning(disable : 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen -# define snprintf _snprintf +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) #endif -#ifdef __clang__ -# pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -# pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants ok. -# pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference it. -# pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // -# if __has_warning("-Wcomma") -# pragma clang diagnostic ignored "-Wcomma" // warning : possible misuse of comma operator here // -# endif -# if __has_warning("-Wreserved-id-macro") -# pragma clang diagnostic ignored "-Wreserved-id-macro" // warning : macro name is a reserved identifier // -# endif -# if __has_warning("-Wdouble-promotion") -# pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -# endif +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wcomma" // warning: possible misuse of comma operator here +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #elif defined(__GNUC__) -# pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used -# pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -# pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value -# pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'xxxx' to type 'xxxx' casts away qualifiers +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wstack-protector" // warning: stack protector not protecting local variables: variable length buffer +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead #endif //------------------------------------------------------------------------- -// STB libraries implementation +// [SECTION] STB libraries implementation (for stb_truetype and stb_rect_pack) //------------------------------------------------------------------------- -// #define IMGUI_STB_NAMESPACE ImGuiStb -// #define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION -// #define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +// Compile time options: +//#define IMGUI_STB_NAMESPACE ImStb +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION #ifdef IMGUI_STB_NAMESPACE namespace IMGUI_STB_NAMESPACE @@ -72,4938 +92,4457 @@ namespace IMGUI_STB_NAMESPACE #endif #ifdef _MSC_VER -# pragma warning(push) -# pragma warning(disable : 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (push) +#pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (disable: 6011) // (stb_rectpack) Dereferencing NULL pointer 'cur->next'. +#pragma warning (disable: 6385) // (stb_truetype) Reading invalid data from 'buffer': the readable size is '_Old_3`kernel_width' bytes, but '3' bytes may be read. +#pragma warning (disable: 28182) // (stb_rectpack) Dereferencing NULL pointer. 'cur' contains the same NULL value as 'cur->next' did. #endif -#ifdef __clang__ -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wunused-function" -# pragma clang diagnostic ignored "-Wmissing-prototypes" -# pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#pragma clang diagnostic ignored "-Wcast-qual" // warning: cast from 'const xxxx *' to 'xxx *' drops const qualifier #endif -#ifdef __GNUC__ -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +#ifndef STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBRP_STATIC +#define STBRP_ASSERT(x) do { IM_ASSERT(x); } while (0) +#define STBRP_SORT ImQsort +#define STB_RECT_PACK_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_RECT_PACK_FILENAME +#include IMGUI_STB_RECT_PACK_FILENAME +#else +#include "imstb_rectpack.h" +#endif #endif -#define STBRP_ASSERT(x) IM_ASSERT(x) -#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION -# define STBRP_STATIC -# define STB_RECT_PACK_IMPLEMENTATION +#ifdef IMGUI_ENABLE_STB_TRUETYPE +#ifndef STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBTT_malloc(x,u) ((void)(u), IM_ALLOC(x)) +#define STBTT_free(x,u) ((void)(u), IM_FREE(x)) +#define STBTT_assert(x) do { IM_ASSERT(x); } while(0) +#define STBTT_fmod(x,y) ImFmod(x,y) +#define STBTT_sqrt(x) ImSqrt(x) +#define STBTT_pow(x,y) ImPow(x,y) +#define STBTT_fabs(x) ImFabs(x) +#define STBTT_ifloor(x) ((int)ImFloor(x)) +#define STBTT_iceil(x) ((int)ImCeil(x)) +#define STBTT_STATIC +#define STB_TRUETYPE_IMPLEMENTATION +#else +#define STBTT_DEF extern #endif -#include "stb_rect_pack.h" - -#define STBTT_malloc(x, u) ((void) (u), ImGui::MemAlloc(x)) -#define STBTT_free(x, u) ((void) (u), ImGui::MemFree(x)) -#define STBTT_assert(x) IM_ASSERT(x) -#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION -# define STBTT_STATIC -# define STB_TRUETYPE_IMPLEMENTATION +#ifdef IMGUI_STB_TRUETYPE_FILENAME +#include IMGUI_STB_TRUETYPE_FILENAME #else -# define STBTT_DEF extern +#include "imstb_truetype.h" +#endif +#endif +#endif // IMGUI_ENABLE_STB_TRUETYPE + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif + +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImStb +using namespace IMGUI_STB_NAMESPACE; #endif -#include "stb_truetype.h" -#ifdef __GNUC__ -# pragma GCC diagnostic pop -#endif +//----------------------------------------------------------------------------- +// [SECTION] Style functions +//----------------------------------------------------------------------------- + +void ImGui::StyleColorsDark(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.29f, 0.48f, 0.54f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.29f, 0.48f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.20f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); +} + +void ImGui::StyleColorsClassic(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); + colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.47f, 0.47f, 0.69f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.41f, 0.64f, 0.69f); + colors[ImGuiCol_TitleBg] = ImVec4(0.27f, 0.27f, 0.54f, 0.83f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.32f, 0.32f, 0.63f, 0.87f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.40f, 0.40f, 0.80f, 0.30f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); + colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.48f, 0.71f, 0.79f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.46f, 0.54f, 0.80f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.40f, 0.40f, 0.90f, 0.45f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.45f, 0.45f, 0.90f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.53f, 0.53f, 0.87f, 0.80f); + colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.27f, 0.27f, 0.38f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.45f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +// Those light colors are better suited with a thicker font than the default one + FrameBorder +void ImGui::StyleColorsLight(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.00f, 0.30f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.00f, 1.00f, 1.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.98f, 0.98f, 0.98f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 0.80f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 0.62f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.35f, 0.35f, 0.35f, 0.17f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.90f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.26f, 0.59f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.78f, 0.87f, 0.98f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.57f, 0.57f, 0.64f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.68f, 0.68f, 0.74f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.30f, 0.30f, 0.30f, 0.09f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList +//----------------------------------------------------------------------------- + +ImDrawListSharedData::ImDrawListSharedData() +{ + memset(this, 0, sizeof(*this)); + for (int i = 0; i < IM_ARRAYSIZE(ArcFastVtx); i++) + { + const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(ArcFastVtx); + ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a)); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) +{ + if (CircleSegmentMaxError == max_error) + return; + + IM_ASSERT(max_error > 0.0f); + CircleSegmentMaxError = max_error; + for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) + { + const float radius = (float)i; + CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : IM_DRAWLIST_ARCFAST_SAMPLE_MAX); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +// Initialize before use in a new frame. We always have a command ready in the buffer. +// In the majority of cases, you would want to call PushClipRect() and PushTextureID() after this. +void ImDrawList::_ResetForNewFrame() +{ + // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. + IM_STATIC_ASSERT(offsetof(ImDrawCmd, ClipRect) == 0); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, TextureId) == sizeof(ImVec4)); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); + if (_Splitter._Count > 1) + _Splitter.Merge(this); + + CmdBuffer.resize(0); + IdxBuffer.resize(0); + VtxBuffer.resize(0); + Flags = _Data->InitialFlags; + memset(&_CmdHeader, 0, sizeof(_CmdHeader)); + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.resize(0); + _TextureIdStack.resize(0); + _Path.resize(0); + _Splitter.Clear(); + CmdBuffer.push_back(ImDrawCmd()); + _FringeScale = 1.0f; +} + +void ImDrawList::_ClearFreeMemory() +{ + CmdBuffer.clear(); + IdxBuffer.clear(); + VtxBuffer.clear(); + Flags = ImDrawListFlags_None; + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.clear(); + _TextureIdStack.clear(); + _Path.clear(); + _Splitter.ClearFreeMemory(); +} + +ImDrawList* ImDrawList::CloneOutput() const +{ + ImDrawList* dst = IM_NEW(ImDrawList(_Data)); + dst->CmdBuffer = CmdBuffer; + dst->IdxBuffer = IdxBuffer; + dst->VtxBuffer = VtxBuffer; + dst->Flags = Flags; + return dst; +} + +void ImDrawList::AddDrawCmd() +{ + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = _CmdHeader.ClipRect; // Same as calling ImDrawCmd_HeaderCopy() + draw_cmd.TextureId = _CmdHeader.TextureId; + draw_cmd.VtxOffset = _CmdHeader.VtxOffset; + draw_cmd.IdxOffset = IdxBuffer.Size; + + IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); + CmdBuffer.push_back(draw_cmd); +} + +// Pop trailing draw command (used before merging or presenting to user) +// Note that this leaves the ImDrawList in a state unfit for further commands, as most code assume that CmdBuffer.Size > 0 && CmdBuffer.back().UserCallback == NULL +void ImDrawList::_PopUnusedDrawCmd() +{ + while (CmdBuffer.Size > 0) + { + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 || curr_cmd->UserCallback != NULL) + return;// break; + CmdBuffer.pop_back(); + } +} + +void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + IM_ASSERT(curr_cmd->UserCallback == NULL); + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + } + curr_cmd->UserCallback = callback; + curr_cmd->UserCallbackData = callback_data; + + AddDrawCmd(); // Force a new command after us (see comment below) +} + +// Compare ClipRect, TextureId and VtxOffset with a single memcmp() +#define ImDrawCmd_HeaderSize (offsetof(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) +#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize)) // Compare ClipRect, TextureId, VtxOffset +#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize)) // Copy ClipRect, TextureId, VtxOffset +#define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset) + +// Try to merge two last draw commands +void ImDrawList::_TryMergeDrawCmds() +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (ImDrawCmd_HeaderCompare(curr_cmd, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && curr_cmd->UserCallback == NULL && prev_cmd->UserCallback == NULL) + { + prev_cmd->ElemCount += curr_cmd->ElemCount; + CmdBuffer.pop_back(); + } +} + +// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. +// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. +void ImDrawList::_OnChangedClipRect() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &_CmdHeader.ClipRect, sizeof(ImVec4)) != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + curr_cmd->ClipRect = _CmdHeader.ClipRect; +} + +void ImDrawList::_OnChangedTextureID() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != _CmdHeader.TextureId) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + curr_cmd->TextureId = _CmdHeader.TextureId; +} + +void ImDrawList::_OnChangedVtxOffset() +{ + // We don't need to compare curr_cmd->VtxOffset != _CmdHeader.VtxOffset because we know it'll be different at the time we call this. + _VtxCurrentIdx = 0; + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + //IM_ASSERT(curr_cmd->VtxOffset != _CmdHeader.VtxOffset); // See #3349 + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + curr_cmd->VtxOffset = _CmdHeader.VtxOffset; +} + +int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const +{ + // Automatic segment count + const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy + if (radius_idx >= 0 && radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + return _Data->CircleSegmentCounts[radius_idx]; // Use cached value + else + return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); +} + +// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) +void ImDrawList::PushClipRect(const ImVec2& cr_min, const ImVec2& cr_max, bool intersect_with_current_clip_rect) +{ + ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); + if (intersect_with_current_clip_rect) + { + ImVec4 current = _CmdHeader.ClipRect; + if (cr.x < current.x) cr.x = current.x; + if (cr.y < current.y) cr.y = current.y; + if (cr.z > current.z) cr.z = current.z; + if (cr.w > current.w) cr.w = current.w; + } + cr.z = ImMax(cr.x, cr.z); + cr.w = ImMax(cr.y, cr.w); + + _ClipRectStack.push_back(cr); + _CmdHeader.ClipRect = cr; + _OnChangedClipRect(); +} + +void ImDrawList::PushClipRectFullScreen() +{ + PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); +} + +void ImDrawList::PopClipRect() +{ + _ClipRectStack.pop_back(); + _CmdHeader.ClipRect = (_ClipRectStack.Size == 0) ? _Data->ClipRectFullscreen : _ClipRectStack.Data[_ClipRectStack.Size - 1]; + _OnChangedClipRect(); +} + +void ImDrawList::PushTextureID(ImTextureID texture_id) +{ + _TextureIdStack.push_back(texture_id); + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +void ImDrawList::PopTextureID() +{ + _TextureIdStack.pop_back(); + _CmdHeader.TextureId = (_TextureIdStack.Size == 0) ? (ImTextureID)NULL : _TextureIdStack.Data[_TextureIdStack.Size - 1]; + _OnChangedTextureID(); +} + +// This is used by ImGui::PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTextureID()/PopTextureID(). +void ImDrawList::_SetTextureID(ImTextureID texture_id) +{ + if (_CmdHeader.TextureId == texture_id) + return; + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +// Reserve space for a number of vertices and indices. +// You must finish filling your reserved data before calling PrimReserve() again, as it may reallocate or +// submit the intermediate results. PrimUnreserve() can be used to release unused allocations. +void ImDrawList::PrimReserve(int idx_count, int vtx_count) +{ + // Large mesh support (when enabled) + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + if (sizeof(ImDrawIdx) == 2 && (_VtxCurrentIdx + vtx_count >= (1 << 16)) && (Flags & ImDrawListFlags_AllowVtxOffset)) + { + // FIXME: In theory we should be testing that vtx_count <64k here. + // In practice, RenderText() relies on reserving ahead for a worst case scenario so it is currently useful for us + // to not make that check until we rework the text functions to handle clipping and large horizontal lines better. + _CmdHeader.VtxOffset = VtxBuffer.Size; + _OnChangedVtxOffset(); + } + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount += idx_count; + + int vtx_buffer_old_size = VtxBuffer.Size; + VtxBuffer.resize(vtx_buffer_old_size + vtx_count); + _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; +} + +// Release the number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). +void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) +{ + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount -= idx_count; + VtxBuffer.shrink(VtxBuffer.Size - vtx_count); + IdxBuffer.shrink(IdxBuffer.Size - idx_count); +} + +// Fully unrolled with inline call to keep our debug builds decently fast. +void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv(_Data->TexUvWhitePixel); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimRectUV(const ImVec2& a, const ImVec2& c, const ImVec2& uv_a, const ImVec2& uv_c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) +{ + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superfluous function calls to optimize debug/non-inlined builds. +// - Those macros expects l-values and need to be used as their own statement. +// - Those macros are intentionally not surrounded by the 'do {} while (0)' idiom because even that translates to runtime with debug compilers. +#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = ImRsqrt(d2); VX *= inv_len; VY *= inv_len; } } (void)0 +#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366) +#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0 + +// TODO: Thickness anti-aliased lines cap are missing their AA fringe. +// We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds. +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness) +{ + if (points_count < 2 || (col & IM_COL32_A_MASK) == 0) + return; + + const bool closed = (flags & ImDrawFlags_Closed) != 0; + const ImVec2 opaque_uv = _Data->TexUvWhitePixel; + const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw + const bool thick_line = (thickness > _FringeScale); + + if (Flags & ImDrawListFlags_AntiAliasedLines) + { + // Anti-aliased stroke + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + // Thicknesses <1.0 should behave like thickness 1.0 + thickness = ImMax(thickness, 1.0f); + const int integer_thickness = (int)thickness; + const float fractional_thickness = thickness - integer_thickness; + + // Do we want to draw this line using a texture? + // - For now, only draw integer-width lines using textures to avoid issues with the way scaling occurs, could be improved. + // - If AA_SIZE is not 1.0f we cannot use the texture path. + const bool use_texture = (Flags & ImDrawListFlags_AntiAliasedLinesUseTex) && (integer_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX) && (fractional_thickness <= 0.00001f) && (AA_SIZE == 1.0f); + + // We should never hit this, because NewFrame() doesn't set ImDrawListFlags_AntiAliasedLinesUseTex unless ImFontAtlasFlags_NoBakedLines is off + IM_ASSERT_PARANOID(!use_texture || !(_Data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)); + + const int idx_count = use_texture ? (count * 6) : (thick_line ? count * 18 : count * 12); + const int vtx_count = use_texture ? (points_count * 2) : (thick_line ? points_count * 4 : points_count * 3); + PrimReserve(idx_count, vtx_count); + + // Temporary buffer + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + _Data->TempBuffer.reserve_discard(points_count * ((use_texture || !thick_line) ? 3 : 5)); + ImVec2* temp_normals = _Data->TempBuffer.Data; + ImVec2* temp_points = temp_normals + points_count; + + // Calculate normals (tangents) for each line segment + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + float dx = points[i2].x - points[i1].x; + float dy = points[i2].y - points[i1].y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i1].x = dy; + temp_normals[i1].y = -dx; + } + if (!closed) + temp_normals[points_count - 1] = temp_normals[points_count - 2]; + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (use_texture || !thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + // [PATH 2] Non texture-based lines (non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA. + // - In the texture-based path, we don't use AA_SIZE here because the +1 is tied to the generated texture + // (see ImFontAtlasBuildRenderLinesTexData() function), and so alternate values won't work without changes to that code. + // - In the non texture-based paths, we would allow AA_SIZE to potentially be != 1.0f with a patch (e.g. fringe_scale patch to + // allow scaling geometry while preserving one-screen-pixel AA fringe). + const float half_draw_size = use_texture ? ((thickness * 0.5f) + 1) : AA_SIZE; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * half_draw_size; + temp_points[1] = points[0] - temp_normals[0] * half_draw_size; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * half_draw_size; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * half_draw_size; + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; // i2 is the second point of the line segment + const unsigned int idx2 = ((i1 + 1) == points_count) ? _VtxCurrentIdx : (idx1 + (use_texture ? 2 : 3)); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= half_draw_size; // dm_x, dm_y are offset to the outer edge of the AA area + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + ImVec2* out_vtx = &temp_points[i2 * 2]; + out_vtx[0].x = points[i2].x + dm_x; + out_vtx[0].y = points[i2].y + dm_y; + out_vtx[1].x = points[i2].x - dm_x; + out_vtx[1].y = points[i2].y - dm_y; + + if (use_texture) + { + // Add indices for two triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 1); // Right tri + _IdxWritePtr[3] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[4] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Left tri + _IdxWritePtr += 6; + } + else + { + // Add indexes for four triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); // Right tri 1 + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Right tri 2 + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); // Left tri 1 + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); // Left tri 2 + _IdxWritePtr += 12; + } + + idx1 = idx2; + } + + // Add vertexes for each point on the line + if (use_texture) + { + // If we're using textures we only need to emit the left/right edge vertices + ImVec4 tex_uvs = _Data->TexUvLines[integer_thickness]; + /*if (fractional_thickness != 0.0f) // Currently always zero when use_texture==false! + { + const ImVec4 tex_uvs_1 = _Data->TexUvLines[integer_thickness + 1]; + tex_uvs.x = tex_uvs.x + (tex_uvs_1.x - tex_uvs.x) * fractional_thickness; // inlined ImLerp() + tex_uvs.y = tex_uvs.y + (tex_uvs_1.y - tex_uvs.y) * fractional_thickness; + tex_uvs.z = tex_uvs.z + (tex_uvs_1.z - tex_uvs.z) * fractional_thickness; + tex_uvs.w = tex_uvs.w + (tex_uvs_1.w - tex_uvs.w) * fractional_thickness; + }*/ + ImVec2 tex_uv0(tex_uvs.x, tex_uvs.y); + ImVec2 tex_uv1(tex_uvs.z, tex_uvs.w); + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 2 + 0]; _VtxWritePtr[0].uv = tex_uv0; _VtxWritePtr[0].col = col; // Left-side outer edge + _VtxWritePtr[1].pos = temp_points[i * 2 + 1]; _VtxWritePtr[1].uv = tex_uv1; _VtxWritePtr[1].col = col; // Right-side outer edge + _VtxWritePtr += 2; + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; // Center of line + _VtxWritePtr[1].pos = temp_points[i * 2 + 0]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col_trans; // Left-side outer edge + _VtxWritePtr[2].pos = temp_points[i * 2 + 1]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col_trans; // Right-side outer edge + _VtxWritePtr += 3; + } + } + } + else + { + // [PATH 2] Non texture-based lines (thick): we need to draw the solid line core and thus require four vertices per point + const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + const int points_last = points_count - 1; + temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); + temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); + temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 0] = points[points_last] + temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 1] = points[points_last] + temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 2] = points[points_last] - temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 3] = points[points_last] - temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : (i1 + 1); // i2 is the second point of the line segment + const unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : (idx1 + 4); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + float dm_out_x = dm_x * (half_inner_thickness + AA_SIZE); + float dm_out_y = dm_y * (half_inner_thickness + AA_SIZE); + float dm_in_x = dm_x * half_inner_thickness; + float dm_in_y = dm_y * half_inner_thickness; + + // Add temporary vertices + ImVec2* out_vtx = &temp_points[i2 * 4]; + out_vtx[0].x = points[i2].x + dm_out_x; + out_vtx[0].y = points[i2].y + dm_out_y; + out_vtx[1].x = points[i2].x + dm_in_x; + out_vtx[1].y = points[i2].y + dm_in_y; + out_vtx[2].x = points[i2].x - dm_in_x; + out_vtx[2].y = points[i2].y - dm_in_y; + out_vtx[3].x = points[i2].x - dm_out_x; + out_vtx[3].y = points[i2].y - dm_out_y; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[13] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[14] = (ImDrawIdx)(idx1 + 3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1 + 3); _IdxWritePtr[16] = (ImDrawIdx)(idx2 + 3); _IdxWritePtr[17] = (ImDrawIdx)(idx2 + 2); + _IdxWritePtr += 18; + + idx1 = idx2; + } + + // Add vertices + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 4 + 0]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i * 4 + 1]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i * 4 + 2]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i * 4 + 3]; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr += 4; + } + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // [PATH 4] Non texture-based, Non anti-aliased lines + const int idx_count = count * 6; + const int vtx_count = count * 4; // FIXME-OPT: Not sharing edges + PrimReserve(idx_count, vtx_count); + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + const ImVec2& p1 = points[i1]; + const ImVec2& p2 = points[i2]; + + float dx = p2.x - p1.x; + float dy = p2.y - p1.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + dx *= (thickness * 0.5f); + dy *= (thickness * 0.5f); + + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + 2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx + 2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx + 3); + _IdxWritePtr += 6; + _VtxCurrentIdx += 4; + } + } +} + +// - We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. +// - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2)*3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + ((i - 1) << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (i << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2)*3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + i - 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + i); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +void ImDrawList::_PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + // Calculate arc auto segment step size + if (a_step <= 0) + a_step = IM_DRAWLIST_ARCFAST_SAMPLE_MAX / _CalcCircleAutoSegmentCount(radius); + + // Make sure we never do steps larger than one quarter of the circle + a_step = ImClamp(a_step, 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); + + const int sample_range = ImAbs(a_max_sample - a_min_sample); + const int a_next_step = a_step; + + int samples = sample_range + 1; + bool extra_max_sample = false; + if (a_step > 1) + { + samples = sample_range / a_step + 1; + const int overstep = sample_range % a_step; + + if (overstep > 0) + { + extra_max_sample = true; + samples++; + + // When we have overstep to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + if (sample_range > 0) + a_step -= (a_step - overstep) / 2; + } + } + + _Path.resize(_Path.Size + samples); + ImVec2* out_ptr = _Path.Data + (_Path.Size - samples); + + int sample_index = a_min_sample; + if (sample_index < 0 || sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + { + sample_index = sample_index % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + } + + if (a_max_sample >= a_min_sample) + { + for (int a = a_min_sample; a <= a_max_sample; a += a_step, sample_index += a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + sample_index -= IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + else + { + for (int a = a_min_sample; a >= a_max_sample; a -= a_step, sample_index -= a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + + if (extra_max_sample) + { + int normalized_max_sample = a_max_sample % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (normalized_max_sample < 0) + normalized_max_sample += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[normalized_max_sample]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + + IM_ASSERT_PARANOID(_Path.Data + _Path.Size == out_ptr); +} + +void ImDrawList::_PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + // Note that we are adding a point at both a_min and a_max. + // If you are trying to draw a full closed circle you don't want the overlapping points! + _Path.reserve(_Path.Size + (num_segments + 1)); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + _Path.push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius)); + } +} + +// 0: East, 3: South, 6: West, 9: North, 12: East +void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + _PathArcToFastEx(center, radius, a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0); +} + +void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + if (num_segments > 0) + { + _PathArcToN(center, radius, a_min, a_max, num_segments); + return; + } + + // Automatic segment count + if (radius <= _Data->ArcFastRadiusCutoff) + { + const bool a_is_reverse = a_max < a_min; + + // We are going to use precomputed values for mid samples. + // Determine first and last sample in lookup table that belong to the arc. + const float a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0f); + const float a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0f); + + const int a_min_sample = a_is_reverse ? (int)ImFloor(a_min_sample_f) : (int)ImCeil(a_min_sample_f); + const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloor(a_max_sample_f); + const int a_mid_samples = a_is_reverse ? ImMax(a_min_sample - a_max_sample, 0) : ImMax(a_max_sample - a_min_sample, 0); + + const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const float a_max_segment_angle = a_max_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const bool a_emit_start = ImAbs(a_min_segment_angle - a_min) >= 1e-5f; + const bool a_emit_end = ImAbs(a_max - a_max_segment_angle) >= 1e-5f; + + _Path.reserve(_Path.Size + (a_mid_samples + 1 + (a_emit_start ? 1 : 0) + (a_emit_end ? 1 : 0))); + if (a_emit_start) + _Path.push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius)); + if (a_mid_samples > 0) + _PathArcToFastEx(center, radius, a_min_sample, a_max_sample, 0); + if (a_emit_end) + _Path.push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius)); + } + else + { + const float arc_length = ImAbs(a_max - a_min); + const int circle_segment_count = _CalcCircleAutoSegmentCount(radius); + const int arc_segment_count = ImMax((int)ImCeil(circle_segment_count * arc_length / (IM_PI * 2.0f)), (int)(2.0f * IM_PI / arc_length)); + _PathArcToN(center, radius, a_min, a_max, arc_segment_count); + } +} + +void ImDrawList::PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments) +{ + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + _Path.reserve(_Path.Size + (num_segments + 1)); + + const float cos_rot = ImCos(rot); + const float sin_rot = ImSin(rot); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + ImVec2 point(ImCos(a) * radius.x, ImSin(a) * radius.y); + const ImVec2 rel((point.x * cos_rot) - (point.y * sin_rot), (point.x * sin_rot) + (point.y * cos_rot)); + point.x = rel.x + center.x; + point.y = rel.y + center.y; + _Path.push_back(point); + } +} + +ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t) +{ + float u = 1.0f - t; + float w1 = u * u * u; + float w2 = 3 * u * u * t; + float w3 = 3 * u * t * t; + float w4 = t * t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y); +} + +ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t) +{ + float u = 1.0f - t; + float w1 = u * u; + float w2 = 2 * u * t; + float w3 = t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x, w1 * p1.y + w2 * p2.y + w3 * p3.y); +} + +// Closely mimics ImBezierCubicClosestPointCasteljau() in imgui.cpp +static void PathBezierCubicCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = (x2 - x4) * dy - (y2 - y4) * dx; + float d3 = (x3 - x4) * dy - (y3 - y4) * dx; + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x4, y4)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x34 = (x3 + x4) * 0.5f, y34 = (y3 + y4) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + float x234 = (x23 + x34) * 0.5f, y234 = (y23 + y34) * 0.5f; + float x1234 = (x123 + x234) * 0.5f, y1234 = (y123 + y234) * 0.5f; + PathBezierCubicCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + PathBezierCubicCurveToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +static void PathBezierQuadraticCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float tess_tol, int level) +{ + float dx = x3 - x1, dy = y3 - y1; + float det = (x2 - x3) * dy - (y2 - y3) * dx; + if (det * det * 4.0f < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x3, y3)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + PathBezierQuadraticCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, tess_tol, level + 1); + PathBezierQuadraticCurveToCasteljau(path, x123, y123, x23, y23, x3, y3, tess_tol, level + 1); + } +} + +void ImDrawList::PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierCubicCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); // Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step)); + } +} + +void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierQuadraticCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, _Data->CurveTessellationTol, 0);// Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierQuadraticCalc(p1, p2, p3, t_step * i_step)); + } +} + +static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) +{ + /* + IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Obsoleted in 1.82 (from February 2021). This code was stripped/simplified and mostly commented in 1.90 (from September 2023) + // - Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) + if (flags == ~0) { return ImDrawFlags_RoundCornersAll; } + // - Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations). Read details in older version of this code. + if (flags >= 0x01 && flags <= 0x0F) { return (flags << 4); } + // We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f' +#endif + */ + // If this assert triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. + // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc. anyway. + // See details in 1.82 Changelog as well as 2021/03/12 and 2023/09/08 entries in "API BREAKING CHANGES" section. + IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); + + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + return flags; +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) +{ + if (rounding >= 0.5f) + { + flags = FixRectCornerFlags(flags); + rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + } + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PathLineTo(a); + PathLineTo(ImVec2(b.x, a.y)); + PathLineTo(b); + PathLineTo(ImVec2(a.x, b.y)); + } + else + { + const float rounding_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? rounding : 0.0f; + const float rounding_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? rounding : 0.0f; + const float rounding_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? rounding : 0.0f; + const float rounding_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? rounding : 0.0f; + PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); + PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); + PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); + PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); + } +} + +void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + PathLineTo(p1 + ImVec2(0.5f, 0.5f)); + PathLineTo(p2 + ImVec2(0.5f, 0.5f)); + PathStroke(col, 0, thickness); +} + +// p_min = upper-left, p_max = lower-right +// Note we don't render 1 pixels sized rectangles properly. +void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (Flags & ImDrawListFlags_AntiAliasedLines) + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags); + else + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PrimReserve(6, 4); + PrimRect(p_min, p_max, col); + } + else + { + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + } +} + +// p_min = upper-left, p_max = lower-right +void ImDrawList::AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + PrimReserve(6, 4); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 3)); + PrimWriteVtx(p_min, uv, col_upr_left); + PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv, col_upr_right); + PrimWriteVtx(p_max, uv, col_bot_right); + PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv, col_bot_left); +} + +void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathFillConvex(col); +} + +void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathFillConvex(col); +} + +void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius - 0.5f, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + } + + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + } + + PathFillConvex(col); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Ellipse +void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Cubic Bezier takes 4 controls points +void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierCubicCurveTo(p2, p3, p4, num_segments); + PathStroke(col, 0, thickness); +} + +// Quadratic Bezier takes 3 controls points +void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierQuadraticCurveTo(p2, p3, num_segments); + PathStroke(col, 0, thickness); +} + +void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + // Accept null ranges + if (text_begin == text_end || text_begin[0] == 0) + return; + if (text_end == NULL) + text_end = text_begin + strlen(text_begin); + + // Pull default font/size from the shared ImDrawListSharedData instance + if (font == NULL) + font = _Data->Font; + if (font_size == 0.0f) + font_size = _Data->FontSize; + + IM_ASSERT(font->ContainerAtlas->TexID == _CmdHeader.TextureId); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. + + ImVec4 clip_rect = _CmdHeader.ClipRect; + if (cpu_fine_clip_rect) + { + clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); + clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect->y); + clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); + clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); + } + font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); +} + +void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +{ + AddText(NULL, 0.0f, pos, col, text_begin, text_end); +} + +void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimRectUV(p_min, p_max, uv_min, uv_max, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimQuadUV(p1, p2, p3, p4, uv1, uv2, uv3, uv4, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + flags = FixRectCornerFlags(flags); + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); + return; + } + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + int vert_start_idx = VtxBuffer.Size; + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + int vert_end_idx = VtxBuffer.Size; + ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true); + + if (push_texture_id) + PopTextureID(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +//----------------------------------------------------------------------------- +// Triangulate concave polygons. Based on "Triangulation by Ear Clipping" paper, O(N^2) complexity. +// Reference: https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf +// Provided as a convenience for user but not used by main library. +//----------------------------------------------------------------------------- +// - ImTriangulator [Internal] +// - AddConcavePolyFilled() +//----------------------------------------------------------------------------- + +enum ImTriangulatorNodeType +{ + ImTriangulatorNodeType_Convex, + ImTriangulatorNodeType_Ear, + ImTriangulatorNodeType_Reflex +}; + +struct ImTriangulatorNode +{ + ImTriangulatorNodeType Type; + int Index; + ImVec2 Pos; + ImTriangulatorNode* Next; + ImTriangulatorNode* Prev; + + void Unlink() { Next->Prev = Prev; Prev->Next = Next; } +}; + +struct ImTriangulatorNodeSpan +{ + ImTriangulatorNode** Data = NULL; + int Size = 0; + + void push_back(ImTriangulatorNode* node) { Data[Size++] = node; } + void find_erase_unsorted(int idx) { for (int i = Size - 1; i >= 0; i--) if (Data[i]->Index == idx) { Data[i] = Data[Size - 1]; Size--; return; } } +}; + +struct ImTriangulator +{ + static int EstimateTriangleCount(int points_count) { return (points_count < 3) ? 0 : points_count - 2; } + static int EstimateScratchBufferSize(int points_count) { return sizeof(ImTriangulatorNode) * points_count + sizeof(ImTriangulatorNode*) * points_count * 2; } + + void Init(const ImVec2* points, int points_count, void* scratch_buffer); + void GetNextTriangle(unsigned int out_triangle[3]); // Return relative indexes for next triangle + + // Internal functions + void BuildNodes(const ImVec2* points, int points_count); + void BuildReflexes(); + void BuildEars(); + void FlipNodeList(); + bool IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const; + void ReclassifyNode(ImTriangulatorNode* node); + + // Internal members + int _TrianglesLeft = 0; + ImTriangulatorNode* _Nodes = NULL; + ImTriangulatorNodeSpan _Ears; + ImTriangulatorNodeSpan _Reflexes; +}; + +// Distribute storage for nodes, ears and reflexes. +// FIXME-OPT: if everything is convex, we could report it to caller and let it switch to an convex renderer +// (this would require first building reflexes to bail to convex if empty, without even building nodes) +void ImTriangulator::Init(const ImVec2* points, int points_count, void* scratch_buffer) +{ + IM_ASSERT(scratch_buffer != NULL && points_count >= 3); + _TrianglesLeft = EstimateTriangleCount(points_count); + _Nodes = (ImTriangulatorNode*)scratch_buffer; // points_count x Node + _Ears.Data = (ImTriangulatorNode**)(_Nodes + points_count); // points_count x Node* + _Reflexes.Data = (ImTriangulatorNode**)(_Nodes + points_count) + points_count; // points_count x Node* + BuildNodes(points, points_count); + BuildReflexes(); + BuildEars(); +} + +void ImTriangulator::BuildNodes(const ImVec2* points, int points_count) +{ + for (int i = 0; i < points_count; i++) + { + _Nodes[i].Type = ImTriangulatorNodeType_Convex; + _Nodes[i].Index = i; + _Nodes[i].Pos = points[i]; + _Nodes[i].Next = _Nodes + i + 1; + _Nodes[i].Prev = _Nodes + i - 1; + } + _Nodes[0].Prev = _Nodes + points_count - 1; + _Nodes[points_count - 1].Next = _Nodes; +} + +void ImTriangulator::BuildReflexes() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (ImTriangleIsClockwise(n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Reflex; + _Reflexes.push_back(n1); + } +} + +void ImTriangulator::BuildEars() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (n1->Type != ImTriangulatorNodeType_Convex) + continue; + if (!IsEar(n1->Prev->Index, n1->Index, n1->Next->Index, n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Ear; + _Ears.push_back(n1); + } +} + +void ImTriangulator::GetNextTriangle(unsigned int out_triangle[3]) +{ + if (_Ears.Size == 0) + { + FlipNodeList(); + + ImTriangulatorNode* node = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, node = node->Next) + node->Type = ImTriangulatorNodeType_Convex; + _Reflexes.Size = 0; + BuildReflexes(); + BuildEars(); + + // If we still don't have ears, it means geometry is degenerated. + if (_Ears.Size == 0) + { + // Return first triangle available, mimicking the behavior of convex fill. + IM_ASSERT(_TrianglesLeft > 0); // Geometry is degenerated + _Ears.Data[0] = _Nodes; + _Ears.Size = 1; + } + } + + ImTriangulatorNode* ear = _Ears.Data[--_Ears.Size]; + out_triangle[0] = ear->Prev->Index; + out_triangle[1] = ear->Index; + out_triangle[2] = ear->Next->Index; + + ear->Unlink(); + if (ear == _Nodes) + _Nodes = ear->Next; + + ReclassifyNode(ear->Prev); + ReclassifyNode(ear->Next); + _TrianglesLeft--; +} + +void ImTriangulator::FlipNodeList() +{ + ImTriangulatorNode* prev = _Nodes; + ImTriangulatorNode* temp = _Nodes; + ImTriangulatorNode* current = _Nodes->Next; + prev->Next = prev; + prev->Prev = prev; + while (current != _Nodes) + { + temp = current->Next; + + current->Next = prev; + prev->Prev = current; + _Nodes->Next = current; + current->Prev = _Nodes; + + prev = current; + current = temp; + } + _Nodes = prev; +} + +// A triangle is an ear is no other vertex is inside it. We can test reflexes vertices only (see reference algorithm) +bool ImTriangulator::IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const +{ + ImTriangulatorNode** p_end = _Reflexes.Data + _Reflexes.Size; + for (ImTriangulatorNode** p = _Reflexes.Data; p < p_end; p++) + { + ImTriangulatorNode* reflex = *p; + if (reflex->Index != i0 && reflex->Index != i1 && reflex->Index != i2) + if (ImTriangleContainsPoint(v0, v1, v2, reflex->Pos)) + return false; + } + return true; +} + +void ImTriangulator::ReclassifyNode(ImTriangulatorNode* n1) +{ + // Classify node + ImTriangulatorNodeType type; + const ImTriangulatorNode* n0 = n1->Prev; + const ImTriangulatorNode* n2 = n1->Next; + if (!ImTriangleIsClockwise(n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Reflex; + else if (IsEar(n0->Index, n1->Index, n2->Index, n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Ear; + else + type = ImTriangulatorNodeType_Convex; + + // Update lists when a type changes + if (type == n1->Type) + return; + if (n1->Type == ImTriangulatorNodeType_Reflex) + _Reflexes.find_erase_unsorted(n1->Index); + else if (n1->Type == ImTriangulatorNodeType_Ear) + _Ears.find_erase_unsorted(n1->Index); + if (type == ImTriangulatorNodeType_Reflex) + _Reflexes.push_back(n1); + else if (type == ImTriangulatorNodeType_Ear) + _Ears.push_back(n1); + n1->Type = type; +} + +// Use ear-clipping algorithm to triangulate a simple polygon (no self-interaction, no holes). +// (Reminder: we don't perform any coarse clipping/culling in ImDrawList layer! +// It is up to caller to ensure not making costly calls that will be outside of visible area. +// As concave fill is noticeably more expensive than other primitives, be mindful of this... +// Caller can build AABB of points, and avoid filling if 'draw_list->_CmdHeader.ClipRect.Overlays(points_bb) == false') +void ImDrawList::AddConcavePolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + ImTriangulator triangulator; + unsigned int triangle[3]; + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2) * 3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (triangle[0] << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (triangle[1] << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (triangle[2] << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2) * 3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx + triangle[0]); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + triangle[1]); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + triangle[2]); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawListSplitter +//----------------------------------------------------------------------------- +// FIXME: This may be a little confusing, trying to be a little too low-level/optimal instead of just doing vector swap.. +//----------------------------------------------------------------------------- + +void ImDrawListSplitter::ClearFreeMemory() +{ + for (int i = 0; i < _Channels.Size; i++) + { + if (i == _Current) + memset(&_Channels[i], 0, sizeof(_Channels[i])); // Current channel is a copy of CmdBuffer/IdxBuffer, don't destruct again + _Channels[i]._CmdBuffer.clear(); + _Channels[i]._IdxBuffer.clear(); + } + _Current = 0; + _Count = 1; + _Channels.clear(); +} + +void ImDrawListSplitter::Split(ImDrawList* draw_list, int channels_count) +{ + IM_UNUSED(draw_list); + IM_ASSERT(_Current == 0 && _Count <= 1 && "Nested channel splitting is not supported. Please use separate instances of ImDrawListSplitter."); + int old_channels_count = _Channels.Size; + if (old_channels_count < channels_count) + { + _Channels.reserve(channels_count); // Avoid over reserving since this is likely to stay stable + _Channels.resize(channels_count); + } + _Count = channels_count; + + // Channels[] (24/32 bytes each) hold storage that we'll swap with draw_list->_CmdBuffer/_IdxBuffer + // The content of Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. + // When we switch to the next channel, we'll copy draw_list->_CmdBuffer/_IdxBuffer into Channels[0] and then Channels[1] into draw_list->CmdBuffer/_IdxBuffer + memset(&_Channels[0], 0, sizeof(ImDrawChannel)); + for (int i = 1; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&_Channels[i]) ImDrawChannel(); + } + else + { + _Channels[i]._CmdBuffer.resize(0); + _Channels[i]._IdxBuffer.resize(0); + } + } +} + +void ImDrawListSplitter::Merge(ImDrawList* draw_list) +{ + // Note that we never use or rely on _Channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + if (_Count <= 1) + return; + + SetCurrentChannel(draw_list, 0); + draw_list->_PopUnusedDrawCmd(); + + // Calculate our final buffer sizes. Also fix the incorrect IdxOffset values in each command. + int new_cmd_buffer_count = 0; + int new_idx_buffer_count = 0; + ImDrawCmd* last_cmd = (_Count > 0 && draw_list->CmdBuffer.Size > 0) ? &draw_list->CmdBuffer.back() : NULL; + int idx_offset = last_cmd ? last_cmd->IdxOffset + last_cmd->ElemCount : 0; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (ch._CmdBuffer.Size > 0 && ch._CmdBuffer.back().ElemCount == 0 && ch._CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() + ch._CmdBuffer.pop_back(); + + if (ch._CmdBuffer.Size > 0 && last_cmd != NULL) + { + // Do not include ImDrawCmd_AreSequentialIdxOffset() in the compare as we rebuild IdxOffset values ourselves. + // Manipulating IdxOffset (e.g. by reordering draw commands like done by RenderDimmedBackgroundBehindWindow()) is not supported within a splitter. + ImDrawCmd* next_cmd = &ch._CmdBuffer[0]; + if (ImDrawCmd_HeaderCompare(last_cmd, next_cmd) == 0 && last_cmd->UserCallback == NULL && next_cmd->UserCallback == NULL) + { + // Merge previous channel last draw command with current channel first draw command if matching. + last_cmd->ElemCount += next_cmd->ElemCount; + idx_offset += next_cmd->ElemCount; + ch._CmdBuffer.erase(ch._CmdBuffer.Data); // FIXME-OPT: Improve for multiple merges. + } + } + if (ch._CmdBuffer.Size > 0) + last_cmd = &ch._CmdBuffer.back(); + new_cmd_buffer_count += ch._CmdBuffer.Size; + new_idx_buffer_count += ch._IdxBuffer.Size; + for (int cmd_n = 0; cmd_n < ch._CmdBuffer.Size; cmd_n++) + { + ch._CmdBuffer.Data[cmd_n].IdxOffset = idx_offset; + idx_offset += ch._CmdBuffer.Data[cmd_n].ElemCount; + } + } + draw_list->CmdBuffer.resize(draw_list->CmdBuffer.Size + new_cmd_buffer_count); + draw_list->IdxBuffer.resize(draw_list->IdxBuffer.Size + new_idx_buffer_count); + + // Write commands and indices in order (they are fairly small structures, we don't copy vertices only indices) + ImDrawCmd* cmd_write = draw_list->CmdBuffer.Data + draw_list->CmdBuffer.Size - new_cmd_buffer_count; + ImDrawIdx* idx_write = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size - new_idx_buffer_count; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (int sz = ch._CmdBuffer.Size) { memcpy(cmd_write, ch._CmdBuffer.Data, sz * sizeof(ImDrawCmd)); cmd_write += sz; } + if (int sz = ch._IdxBuffer.Size) { memcpy(idx_write, ch._IdxBuffer.Data, sz * sizeof(ImDrawIdx)); idx_write += sz; } + } + draw_list->_IdxWritePtr = idx_write; + + // Ensure there's always a non-callback draw command trailing the command-buffer + if (draw_list->CmdBuffer.Size == 0 || draw_list->CmdBuffer.back().UserCallback != NULL) + draw_list->AddDrawCmd(); + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); + + _Count = 1; +} + +void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) +{ + IM_ASSERT(idx >= 0 && idx < _Count); + if (_Current == idx) + return; + + // Overwrite ImVector (12/16 bytes), four times. This is merely a silly optimization instead of doing .swap() + memcpy(&_Channels.Data[_Current]._CmdBuffer, &draw_list->CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&_Channels.Data[_Current]._IdxBuffer, &draw_list->IdxBuffer, sizeof(draw_list->IdxBuffer)); + _Current = idx; + memcpy(&draw_list->CmdBuffer, &_Channels.Data[idx]._CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&draw_list->IdxBuffer, &_Channels.Data[idx]._IdxBuffer, sizeof(draw_list->IdxBuffer)); + draw_list->_IdxWritePtr = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size; + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = (draw_list->CmdBuffer.Size == 0) ? NULL : &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd == NULL) + draw_list->AddDrawCmd(); + else if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawData +//----------------------------------------------------------------------------- + +void ImDrawData::Clear() +{ + Valid = false; + CmdListsCount = TotalIdxCount = TotalVtxCount = 0; + CmdLists.resize(0); // The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them. + DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.0f, 0.0f); + OwnerViewport = NULL; +} -#ifdef __clang__ -# pragma clang diagnostic pop -#endif +// Important: 'out_list' is generally going to be draw_data->CmdLists, but may be another temporary list +// as long at it is expected that the result will be later merged into draw_data->CmdLists[]. +void ImGui::AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.Size == 0) + return; + if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) + return; + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. + // May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. + // Be mindful that the lower-level ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. + // - If you want large meshes with more than 64K vertices, you can either: + // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. + // Most example backends already support this from 1.71. Pre-1.71 backends won't. + // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. + // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. + // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. + // 2 and 4 bytes indices are generally supported by most graphics API. + // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching + // the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + // Add to output list + records state in ImDrawData + out_list->push_back(draw_list); + draw_data->CmdListsCount++; + draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; + draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; +} -#ifdef _MSC_VER -# pragma warning(pop) -#endif +void ImDrawData::AddDrawList(ImDrawList* draw_list) +{ + IM_ASSERT(CmdLists.Size == CmdListsCount); + draw_list->_PopUnusedDrawCmd(); + ImGui::AddDrawListToDrawDataEx(this, &CmdLists, draw_list); +} -#ifdef IMGUI_STB_NAMESPACE -} // namespace ImGuiStb -using namespace IMGUI_STB_NAMESPACE; -#endif +// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! +void ImDrawData::DeIndexAllBuffers() +{ + ImVector new_vtx_buffer; + TotalVtxCount = TotalIdxCount = 0; + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + if (cmd_list->IdxBuffer.empty()) + continue; + new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); + for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) + new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; + cmd_list->VtxBuffer.swap(new_vtx_buffer); + cmd_list->IdxBuffer.resize(0); + TotalVtxCount += cmd_list->VtxBuffer.Size; + } +} + +// Helper to scale the ClipRect field of each ImDrawCmd. +// Use if your final output buffer is at a different scale than draw_data->DisplaySize, +// or if there is a difference between your window resolution and framebuffer resolution. +void ImDrawData::ScaleClipRects(const ImVec2& fb_scale) +{ + for (ImDrawList* draw_list : CmdLists) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + cmd.ClipRect = ImVec4(cmd.ClipRect.x * fb_scale.x, cmd.ClipRect.y * fb_scale.y, cmd.ClipRect.z * fb_scale.x, cmd.ClipRect.w * fb_scale.y); +} //----------------------------------------------------------------------------- -// Style functions +// [SECTION] Helpers ShadeVertsXXX functions //----------------------------------------------------------------------------- -void ImGui::StyleColorsDark(ImGuiStyle *dst) -{ - ImGuiStyle *style = dst ? dst : &ImGui::GetStyle(); - ImVec4 *colors = style->Colors; - - colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); - colors[ImGuiCol_ChildBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); - colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.29f, 0.48f, 0.54f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); - colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.29f, 0.48f, 1.00f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); - colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_CloseButton] = ImVec4(0.41f, 0.41f, 0.41f, 0.50f); - colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); - colors[ImGuiCol_CloseButtonActive] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); - colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); - colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); - colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); -} - -void ImGui::StyleColorsClassic(ImGuiStyle *dst) -{ - ImGuiStyle *style = dst ? dst : &ImGui::GetStyle(); - ImVec4 *colors = style->Colors; - - colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.70f); - colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); - colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.47f, 0.47f, 0.69f, 0.40f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.41f, 0.64f, 0.69f); - colors[ImGuiCol_TitleBg] = ImVec4(0.27f, 0.27f, 0.54f, 0.83f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.32f, 0.32f, 0.63f, 0.87f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.40f, 0.40f, 0.80f, 0.30f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); - colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); - colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); - colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.48f, 0.71f, 0.79f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.46f, 0.54f, 0.80f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.40f, 0.40f, 0.90f, 0.45f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.45f, 0.45f, 0.90f, 0.80f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.53f, 0.53f, 0.87f, 0.80f); - colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.16f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); - colors[ImGuiCol_CloseButton] = ImVec4(0.50f, 0.50f, 0.90f, 0.50f); - colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.70f, 0.70f, 0.90f, 0.60f); - colors[ImGuiCol_CloseButtonActive] = ImVec4(0.70f, 0.70f, 0.70f, 1.00f); - colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); - colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); - colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); +// Generic linear color gradient, write to RGB fields, leave A untouched. +void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) +{ + ImVec2 gradient_extent = gradient_p1 - gradient_p0; + float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + const int col0_r = (int)(col0 >> IM_COL32_R_SHIFT) & 0xFF; + const int col0_g = (int)(col0 >> IM_COL32_G_SHIFT) & 0xFF; + const int col0_b = (int)(col0 >> IM_COL32_B_SHIFT) & 0xFF; + const int col_delta_r = ((int)(col1 >> IM_COL32_R_SHIFT) & 0xFF) - col0_r; + const int col_delta_g = ((int)(col1 >> IM_COL32_G_SHIFT) & 0xFF) - col0_g; + const int col_delta_b = ((int)(col1 >> IM_COL32_B_SHIFT) & 0xFF) - col0_b; + for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) + { + float d = ImDot(vert->pos - gradient_p0, gradient_extent); + float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + int r = (int)(col0_r + col_delta_r * t); + int g = (int)(col0_g + col_delta_g * t); + int b = (int)(col0_b + col_delta_b * t); + vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); + } } -// Those light colors are better suited with a thicker font than the default one + FrameBorder -void ImGui::StyleColorsLight(ImGuiStyle *dst) -{ - ImGuiStyle *style = dst ? dst : &ImGui::GetStyle(); - ImVec4 *colors = style->Colors; - - colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); - // colors[ImGuiCol_TextHovered] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - // colors[ImGuiCol_TextActive] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f); - colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f); - colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.00f, 0.30f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); - colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.00f, 1.00f, 1.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.98f, 0.98f, 0.98f, 0.53f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 0.80f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); - colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); - colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.80f, 0.80f, 0.80f, 0.56f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_CloseButton] = ImVec4(0.59f, 0.59f, 0.59f, 0.50f); - colors[ImGuiCol_CloseButtonHovered] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); - colors[ImGuiCol_CloseButtonActive] = ImVec4(0.98f, 0.39f, 0.36f, 1.00f); - colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); - colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); - colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); +// Distribute UV over (a, b) rectangle +void ImGui::ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp) +{ + const ImVec2 size = b - a; + const ImVec2 uv_size = uv_b - uv_a; + const ImVec2 scale = ImVec2( + size.x != 0.0f ? (uv_size.x / size.x) : 0.0f, + size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + if (clamp) + { + const ImVec2 min = ImMin(uv_a, uv_b); + const ImVec2 max = ImMax(uv_a, uv_b); + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = ImClamp(uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale), min, max); + } + else + { + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale); + } +} + +void ImGui::ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out) +{ + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->pos = ImRotate(vertex->pos- pivot_in, cos_a, sin_a) + pivot_out; } //----------------------------------------------------------------------------- -// ImDrawListData +// [SECTION] ImFontConfig //----------------------------------------------------------------------------- -ImDrawListSharedData::ImDrawListSharedData() +ImFontConfig::ImFontConfig() { - Font = NULL; - FontSize = 0.0f; - CurveTessellationTol = 0.0f; - ClipRectFullscreen = ImVec4(-8192.0f, -8192.0f, +8192.0f, +8192.0f); - - // Const data - for (int i = 0; i < IM_ARRAYSIZE(CircleVtx12); i++) - { - const float a = ((float) i * 2 * IM_PI) / (float) IM_ARRAYSIZE(CircleVtx12); - CircleVtx12[i] = ImVec2(cosf(a), sinf(a)); - } + memset(this, 0, sizeof(*this)); + FontDataOwnedByAtlas = true; + OversampleH = 2; + OversampleV = 1; + GlyphMaxAdvanceX = FLT_MAX; + RasterizerMultiply = 1.0f; + RasterizerDensity = 1.0f; + EllipsisChar = (ImWchar)-1; } //----------------------------------------------------------------------------- -// ImDrawList +// [SECTION] ImFontAtlas //----------------------------------------------------------------------------- -void ImDrawList::Clear() -{ - CmdBuffer.resize(0); - IdxBuffer.resize(0); - VtxBuffer.resize(0); - Flags = ImDrawListFlags_AntiAliasedLines | ImDrawListFlags_AntiAliasedFill; - _VtxCurrentIdx = 0; - _VtxWritePtr = NULL; - _IdxWritePtr = NULL; - _ClipRectStack.resize(0); - _TextureIdStack.resize(0); - _Path.resize(0); - _ChannelsCurrent = 0; - _ChannelsCount = 1; - // NB: Do not clear channels so our allocations are re-used after the first frame. -} - -void ImDrawList::ClearFreeMemory() -{ - CmdBuffer.clear(); - IdxBuffer.clear(); - VtxBuffer.clear(); - _VtxCurrentIdx = 0; - _VtxWritePtr = NULL; - _IdxWritePtr = NULL; - _ClipRectStack.clear(); - _TextureIdStack.clear(); - _Path.clear(); - _ChannelsCurrent = 0; - _ChannelsCount = 1; - for (int i = 0; i < _Channels.Size; i++) - { - if (i == 0) - memset(&_Channels[0], 0, sizeof(_Channels[0])); // channel 0 is a copy of CmdBuffer/IdxBuffer, don't destruct again - _Channels[i].CmdBuffer.clear(); - _Channels[i].IdxBuffer.clear(); - } - _Channels.clear(); -} - -// Using macros because C++ is a terrible language, we want guaranteed inline, no code in header, and no overhead in Debug builds -#define GetCurrentClipRect() (_ClipRectStack.Size ? _ClipRectStack.Data[_ClipRectStack.Size - 1] : _Data->ClipRectFullscreen) -#define GetCurrentTextureId() (_TextureIdStack.Size ? _TextureIdStack.Data[_TextureIdStack.Size - 1] : NULL) +// A work of art lies ahead! (. = white layer, X = black layer, others are blank) +// The 2x2 white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. +// (This is used when io.MouseDrawCursor = true) +const int FONT_ATLAS_DEFAULT_TEX_DATA_W = 122; // Actual texture will be 2 times that + 1 spacing. +const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; +static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +{ + "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX - XX XX " + "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X -X..X X..X" + "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X -X...X X...X" + "X - X.X - X.....X - X.....X -X...X - X...X- X..X - X...X X...X " + "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X - X...X...X " + "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX - X.....X " + "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX - X...X " + "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX - X.X " + "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X - X...X " + "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X- X.....X " + "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X- X...X...X " + "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X- X...X X...X " + "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X-X...X X...X" + "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X-X..X X..X" + "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X- XX XX " + "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X--------------" + "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X - " + "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X - " + "X.X X..X - -X.......X- X.......X - XX XX - - X..........X - " + "XX X..X - - X.....X - X.....X - X.X X.X - - X........X - " + " X..X - - X...X - X...X - X..X X..X - - X........X - " + " XX - - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX - " + "------------- - X - X -X.....................X- ------------------- " + " ----------------------------------- X...XXXXXXXXXXXXX...X - " + " - X..X X..X - " + " - X.X X.X - " + " - XX XX - " +}; -void ImDrawList::AddDrawCmd() +static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_COUNT][3] = { - ImDrawCmd draw_cmd; - draw_cmd.ClipRect = GetCurrentClipRect(); - draw_cmd.TextureId = GetCurrentTextureId(); + // Pos ........ Size ......... Offset ...... + { ImVec2( 0,3), ImVec2(12,19), ImVec2( 0, 0) }, // ImGuiMouseCursor_Arrow + { ImVec2(13,0), ImVec2( 7,16), ImVec2( 1, 8) }, // ImGuiMouseCursor_TextInput + { ImVec2(31,0), ImVec2(23,23), ImVec2(11,11) }, // ImGuiMouseCursor_ResizeAll + { ImVec2(21,0), ImVec2( 9,23), ImVec2( 4,11) }, // ImGuiMouseCursor_ResizeNS + { ImVec2(55,18),ImVec2(23, 9), ImVec2(11, 4) }, // ImGuiMouseCursor_ResizeEW + { ImVec2(73,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNESW + { ImVec2(55,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNWSE + { ImVec2(91,0), ImVec2(17,22), ImVec2( 5, 0) }, // ImGuiMouseCursor_Hand + { ImVec2(109,0),ImVec2(13,15), ImVec2( 6, 7) }, // ImGuiMouseCursor_NotAllowed +}; - IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); - CmdBuffer.push_back(draw_cmd); +ImFontAtlas::ImFontAtlas() +{ + memset(this, 0, sizeof(*this)); + TexGlyphPadding = 1; + PackIdMouseCursors = PackIdLines = -1; } -void ImDrawList::AddCallback(ImDrawCallback callback, void *callback_data) +ImFontAtlas::~ImFontAtlas() { - ImDrawCmd *current_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; - if (!current_cmd || current_cmd->ElemCount != 0 || current_cmd->UserCallback != NULL) - { - AddDrawCmd(); - current_cmd = &CmdBuffer.back(); - } - current_cmd->UserCallback = callback; - current_cmd->UserCallbackData = callback_data; - - AddDrawCmd(); // Force a new command after us (see comment below) + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + Clear(); } -// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. -// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. -void ImDrawList::UpdateClipRect() -{ - // If current command is used with different settings we need to add a new command - const ImVec4 curr_clip_rect = GetCurrentClipRect(); - ImDrawCmd *curr_cmd = CmdBuffer.Size > 0 ? &CmdBuffer.Data[CmdBuffer.Size - 1] : NULL; - if (!curr_cmd || (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) != 0) || curr_cmd->UserCallback != NULL) - { - AddDrawCmd(); - return; - } - - // Try to merge with previous command if it matches, else use current command - ImDrawCmd *prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; - if (curr_cmd->ElemCount == 0 && prev_cmd && memcmp(&prev_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) == 0 && prev_cmd->TextureId == GetCurrentTextureId() && prev_cmd->UserCallback == NULL) - CmdBuffer.pop_back(); - else - curr_cmd->ClipRect = curr_clip_rect; -} - -void ImDrawList::UpdateTextureID() -{ - // If current command is used with different settings we need to add a new command - const ImTextureID curr_texture_id = GetCurrentTextureId(); - ImDrawCmd *curr_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; - if (!curr_cmd || (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != curr_texture_id) || curr_cmd->UserCallback != NULL) - { - AddDrawCmd(); - return; - } - - // Try to merge with previous command if it matches, else use current command - ImDrawCmd *prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; - if (curr_cmd->ElemCount == 0 && prev_cmd && prev_cmd->TextureId == curr_texture_id && memcmp(&prev_cmd->ClipRect, &GetCurrentClipRect(), sizeof(ImVec4)) == 0 && prev_cmd->UserCallback == NULL) - CmdBuffer.pop_back(); - else - curr_cmd->TextureId = curr_texture_id; -} - -#undef GetCurrentClipRect -#undef GetCurrentTextureId +void ImFontAtlas::ClearInputData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + for (ImFontConfig& font_cfg : ConfigData) + if (font_cfg.FontData && font_cfg.FontDataOwnedByAtlas) + { + IM_FREE(font_cfg.FontData); + font_cfg.FontData = NULL; + } + + // When clearing this we lose access to the font name and other information used to build the font. + for (ImFont* font : Fonts) + if (font->ConfigData >= ConfigData.Data && font->ConfigData < ConfigData.Data + ConfigData.Size) + { + font->ConfigData = NULL; + font->ConfigDataCount = 0; + } + ConfigData.clear(); + CustomRects.clear(); + PackIdMouseCursors = PackIdLines = -1; + // Important: we leave TexReady untouched +} -// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) -void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) -{ - ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); - if (intersect_with_current_clip_rect && _ClipRectStack.Size) - { - ImVec4 current = _ClipRectStack.Data[_ClipRectStack.Size - 1]; - if (cr.x < current.x) - cr.x = current.x; - if (cr.y < current.y) - cr.y = current.y; - if (cr.z > current.z) - cr.z = current.z; - if (cr.w > current.w) - cr.w = current.w; - } - cr.z = ImMax(cr.x, cr.z); - cr.w = ImMax(cr.y, cr.w); - - _ClipRectStack.push_back(cr); - UpdateClipRect(); +void ImFontAtlas::ClearTexData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + if (TexPixelsAlpha8) + IM_FREE(TexPixelsAlpha8); + if (TexPixelsRGBA32) + IM_FREE(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; + TexPixelsUseColors = false; + // Important: we leave TexReady untouched } -void ImDrawList::PushClipRectFullScreen() +void ImFontAtlas::ClearFonts() { - PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + Fonts.clear_delete(); + TexReady = false; } -void ImDrawList::PopClipRect() +void ImFontAtlas::Clear() { - IM_ASSERT(_ClipRectStack.Size > 0); - _ClipRectStack.pop_back(); - UpdateClipRect(); + ClearInputData(); + ClearTexData(); + ClearFonts(); } -void ImDrawList::PushTextureID(ImTextureID texture_id) +void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) { - _TextureIdStack.push_back(texture_id); - UpdateTextureID(); + // Build atlas on demand + if (TexPixelsAlpha8 == NULL) + Build(); + + *out_pixels = TexPixelsAlpha8; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 1; } -void ImDrawList::PopTextureID() +void ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) { - IM_ASSERT(_TextureIdStack.Size > 0); - _TextureIdStack.pop_back(); - UpdateTextureID(); -} - -void ImDrawList::ChannelsSplit(int channels_count) -{ - IM_ASSERT(_ChannelsCurrent == 0 && _ChannelsCount == 1); - int old_channels_count = _Channels.Size; - if (old_channels_count < channels_count) - _Channels.resize(channels_count); - _ChannelsCount = channels_count; - - // _Channels[] (24/32 bytes each) hold storage that we'll swap with this->_CmdBuffer/_IdxBuffer - // The content of _Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. - // When we switch to the next channel, we'll copy _CmdBuffer/_IdxBuffer into _Channels[0] and then _Channels[1] into _CmdBuffer/_IdxBuffer - memset(&_Channels[0], 0, sizeof(ImDrawChannel)); - for (int i = 1; i < channels_count; i++) - { - if (i >= old_channels_count) - { - IM_PLACEMENT_NEW(&_Channels[i]) - ImDrawChannel(); - } - else - { - _Channels[i].CmdBuffer.resize(0); - _Channels[i].IdxBuffer.resize(0); - } - if (_Channels[i].CmdBuffer.Size == 0) - { - ImDrawCmd draw_cmd; - draw_cmd.ClipRect = _ClipRectStack.back(); - draw_cmd.TextureId = _TextureIdStack.back(); - _Channels[i].CmdBuffer.push_back(draw_cmd); - } - } -} - -void ImDrawList::ChannelsMerge() -{ - // Note that we never use or rely on channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. - if (_ChannelsCount <= 1) - return; - - ChannelsSetCurrent(0); - if (CmdBuffer.Size && CmdBuffer.back().ElemCount == 0) - CmdBuffer.pop_back(); - - int new_cmd_buffer_count = 0, new_idx_buffer_count = 0; - for (int i = 1; i < _ChannelsCount; i++) - { - ImDrawChannel &ch = _Channels[i]; - if (ch.CmdBuffer.Size && ch.CmdBuffer.back().ElemCount == 0) - ch.CmdBuffer.pop_back(); - new_cmd_buffer_count += ch.CmdBuffer.Size; - new_idx_buffer_count += ch.IdxBuffer.Size; - } - CmdBuffer.resize(CmdBuffer.Size + new_cmd_buffer_count); - IdxBuffer.resize(IdxBuffer.Size + new_idx_buffer_count); - - ImDrawCmd *cmd_write = CmdBuffer.Data + CmdBuffer.Size - new_cmd_buffer_count; - _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size - new_idx_buffer_count; - for (int i = 1; i < _ChannelsCount; i++) - { - ImDrawChannel &ch = _Channels[i]; - if (int sz = ch.CmdBuffer.Size) - { - memcpy(cmd_write, ch.CmdBuffer.Data, sz * sizeof(ImDrawCmd)); - cmd_write += sz; - } - if (int sz = ch.IdxBuffer.Size) - { - memcpy(_IdxWritePtr, ch.IdxBuffer.Data, sz * sizeof(ImDrawIdx)); - _IdxWritePtr += sz; - } - } - UpdateClipRect(); // We call this instead of AddDrawCmd(), so that empty channels won't produce an extra draw call. - _ChannelsCount = 1; -} - -void ImDrawList::ChannelsSetCurrent(int idx) -{ - IM_ASSERT(idx < _ChannelsCount); - if (_ChannelsCurrent == idx) - return; - memcpy(&_Channels.Data[_ChannelsCurrent].CmdBuffer, &CmdBuffer, sizeof(CmdBuffer)); // copy 12 bytes, four times - memcpy(&_Channels.Data[_ChannelsCurrent].IdxBuffer, &IdxBuffer, sizeof(IdxBuffer)); - _ChannelsCurrent = idx; - memcpy(&CmdBuffer, &_Channels.Data[_ChannelsCurrent].CmdBuffer, sizeof(CmdBuffer)); - memcpy(&IdxBuffer, &_Channels.Data[_ChannelsCurrent].IdxBuffer, sizeof(IdxBuffer)); - _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size; -} - -// NB: this can be called with negative count for removing primitives (as long as the result does not underflow) -void ImDrawList::PrimReserve(int idx_count, int vtx_count) + // Convert to RGBA32 format on demand + // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp + if (!TexPixelsRGBA32) + { + unsigned char* pixels = NULL; + GetTexDataAsAlpha8(&pixels, NULL, NULL); + if (pixels) + { + TexPixelsRGBA32 = (unsigned int*)IM_ALLOC((size_t)TexWidth * (size_t)TexHeight * 4); + const unsigned char* src = pixels; + unsigned int* dst = TexPixelsRGBA32; + for (int n = TexWidth * TexHeight; n > 0; n--) + *dst++ = IM_COL32(255, 255, 255, (unsigned int)(*src++)); + } + } + + *out_pixels = (unsigned char*)TexPixelsRGBA32; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 4; +} + +ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) { - ImDrawCmd &draw_cmd = CmdBuffer.Data[CmdBuffer.Size - 1]; - draw_cmd.ElemCount += idx_count; + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); + IM_ASSERT(font_cfg->SizePixels > 0.0f); + + // Create new font + if (!font_cfg->MergeMode) + Fonts.push_back(IM_NEW(ImFont)); + else + IM_ASSERT(!Fonts.empty() && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. + + ConfigData.push_back(*font_cfg); + ImFontConfig& new_font_cfg = ConfigData.back(); + if (new_font_cfg.DstFont == NULL) + new_font_cfg.DstFont = Fonts.back(); + if (!new_font_cfg.FontDataOwnedByAtlas) + { + new_font_cfg.FontData = IM_ALLOC(new_font_cfg.FontDataSize); + new_font_cfg.FontDataOwnedByAtlas = true; + memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize); + } - int vtx_buffer_old_size = VtxBuffer.Size; - VtxBuffer.resize(vtx_buffer_old_size + vtx_count); - _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + if (new_font_cfg.DstFont->EllipsisChar == (ImWchar)-1) + new_font_cfg.DstFont->EllipsisChar = font_cfg->EllipsisChar; - int idx_buffer_old_size = IdxBuffer.Size; - IdxBuffer.resize(idx_buffer_old_size + idx_count); - _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; -} + ImFontAtlasUpdateConfigDataPointers(this); -// Fully unrolled with inline call to keep our debug builds decently fast. -void ImDrawList::PrimRect(const ImVec2 &a, const ImVec2 &c, ImU32 col) -{ - ImVec2 b(c.x, a.y), d(a.x, c.y), uv(_Data->TexUvWhitePixel); - ImDrawIdx idx = (ImDrawIdx) _VtxCurrentIdx; - _IdxWritePtr[0] = idx; - _IdxWritePtr[1] = (ImDrawIdx) (idx + 1); - _IdxWritePtr[2] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[3] = idx; - _IdxWritePtr[4] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[5] = (ImDrawIdx) (idx + 3); - _VtxWritePtr[0].pos = a; - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos = b; - _VtxWritePtr[1].uv = uv; - _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos = c; - _VtxWritePtr[2].uv = uv; - _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos = d; - _VtxWritePtr[3].uv = uv; - _VtxWritePtr[3].col = col; - _VtxWritePtr += 4; - _VtxCurrentIdx += 4; - _IdxWritePtr += 6; -} - -void ImDrawList::PrimRectUV(const ImVec2 &a, const ImVec2 &c, const ImVec2 &uv_a, const ImVec2 &uv_c, ImU32 col) -{ - ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); - ImDrawIdx idx = (ImDrawIdx) _VtxCurrentIdx; - _IdxWritePtr[0] = idx; - _IdxWritePtr[1] = (ImDrawIdx) (idx + 1); - _IdxWritePtr[2] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[3] = idx; - _IdxWritePtr[4] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[5] = (ImDrawIdx) (idx + 3); - _VtxWritePtr[0].pos = a; - _VtxWritePtr[0].uv = uv_a; - _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos = b; - _VtxWritePtr[1].uv = uv_b; - _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos = c; - _VtxWritePtr[2].uv = uv_c; - _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos = d; - _VtxWritePtr[3].uv = uv_d; - _VtxWritePtr[3].col = col; - _VtxWritePtr += 4; - _VtxCurrentIdx += 4; - _IdxWritePtr += 6; -} - -void ImDrawList::PrimQuadUV(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, const ImVec2 &uv_a, const ImVec2 &uv_b, const ImVec2 &uv_c, const ImVec2 &uv_d, ImU32 col) -{ - ImDrawIdx idx = (ImDrawIdx) _VtxCurrentIdx; - _IdxWritePtr[0] = idx; - _IdxWritePtr[1] = (ImDrawIdx) (idx + 1); - _IdxWritePtr[2] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[3] = idx; - _IdxWritePtr[4] = (ImDrawIdx) (idx + 2); - _IdxWritePtr[5] = (ImDrawIdx) (idx + 3); - _VtxWritePtr[0].pos = a; - _VtxWritePtr[0].uv = uv_a; - _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos = b; - _VtxWritePtr[1].uv = uv_b; - _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos = c; - _VtxWritePtr[2].uv = uv_c; - _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos = d; - _VtxWritePtr[3].uv = uv_d; - _VtxWritePtr[3].col = col; - _VtxWritePtr += 4; - _VtxCurrentIdx += 4; - _IdxWritePtr += 6; + // Invalidate texture + TexReady = false; + ClearTexData(); + return new_font_cfg.DstFont; } -// TODO: Thickness anti-aliased lines cap are missing their AA fringe. -void ImDrawList::AddPolyline(const ImVec2 *points, const int points_count, ImU32 col, bool closed, float thickness) -{ - if (points_count < 2) - return; - - const ImVec2 uv = _Data->TexUvWhitePixel; - - int count = points_count; - if (!closed) - count = points_count - 1; - - const bool thick_line = thickness > 1.0f; - if (Flags & ImDrawListFlags_AntiAliasedLines) - { - // Anti-aliased stroke - const float AA_SIZE = 1.0f; - const ImU32 col_trans = col & ~IM_COL32_A_MASK; - - const int idx_count = thick_line ? count * 18 : count * 12; - const int vtx_count = thick_line ? points_count * 4 : points_count * 3; - PrimReserve(idx_count, vtx_count); - - // Temporary buffer - ImVec2 *temp_normals = (ImVec2 *) alloca(points_count * (thick_line ? 5 : 3) * sizeof(ImVec2)); - ImVec2 *temp_points = temp_normals + points_count; - - for (int i1 = 0; i1 < count; i1++) - { - const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; - ImVec2 diff = points[i2] - points[i1]; - diff *= ImInvLength(diff, 1.0f); - temp_normals[i1].x = diff.y; - temp_normals[i1].y = -diff.x; - } - if (!closed) - temp_normals[points_count - 1] = temp_normals[points_count - 2]; - - if (!thick_line) - { - if (!closed) - { - temp_points[0] = points[0] + temp_normals[0] * AA_SIZE; - temp_points[1] = points[0] - temp_normals[0] * AA_SIZE; - temp_points[(points_count - 1) * 2 + 0] = points[points_count - 1] + temp_normals[points_count - 1] * AA_SIZE; - temp_points[(points_count - 1) * 2 + 1] = points[points_count - 1] - temp_normals[points_count - 1] * AA_SIZE; - } - - // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. - unsigned int idx1 = _VtxCurrentIdx; - for (int i1 = 0; i1 < count; i1++) - { - const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; - unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : idx1 + 3; - - // Average normals - ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; - float dmr2 = dm.x * dm.x + dm.y * dm.y; - if (dmr2 > 0.000001f) - { - float scale = 1.0f / dmr2; - if (scale > 100.0f) - scale = 100.0f; - dm *= scale; - } - dm *= AA_SIZE; - temp_points[i2 * 2 + 0] = points[i2] + dm; - temp_points[i2 * 2 + 1] = points[i2] - dm; - - // Add indexes - _IdxWritePtr[0] = (ImDrawIdx) (idx2 + 0); - _IdxWritePtr[1] = (ImDrawIdx) (idx1 + 0); - _IdxWritePtr[2] = (ImDrawIdx) (idx1 + 2); - _IdxWritePtr[3] = (ImDrawIdx) (idx1 + 2); - _IdxWritePtr[4] = (ImDrawIdx) (idx2 + 2); - _IdxWritePtr[5] = (ImDrawIdx) (idx2 + 0); - _IdxWritePtr[6] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr[7] = (ImDrawIdx) (idx1 + 1); - _IdxWritePtr[8] = (ImDrawIdx) (idx1 + 0); - _IdxWritePtr[9] = (ImDrawIdx) (idx1 + 0); - _IdxWritePtr[10] = (ImDrawIdx) (idx2 + 0); - _IdxWritePtr[11] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr += 12; - - idx1 = idx2; - } - - // Add vertexes - for (int i = 0; i < points_count; i++) - { - _VtxWritePtr[0].pos = points[i]; - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos = temp_points[i * 2 + 0]; - _VtxWritePtr[1].uv = uv; - _VtxWritePtr[1].col = col_trans; - _VtxWritePtr[2].pos = temp_points[i * 2 + 1]; - _VtxWritePtr[2].uv = uv; - _VtxWritePtr[2].col = col_trans; - _VtxWritePtr += 3; - } - } - else - { - const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; - if (!closed) - { - temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); - temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); - temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); - temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); - temp_points[(points_count - 1) * 4 + 0] = points[points_count - 1] + temp_normals[points_count - 1] * (half_inner_thickness + AA_SIZE); - temp_points[(points_count - 1) * 4 + 1] = points[points_count - 1] + temp_normals[points_count - 1] * (half_inner_thickness); - temp_points[(points_count - 1) * 4 + 2] = points[points_count - 1] - temp_normals[points_count - 1] * (half_inner_thickness); - temp_points[(points_count - 1) * 4 + 3] = points[points_count - 1] - temp_normals[points_count - 1] * (half_inner_thickness + AA_SIZE); - } - - // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. - unsigned int idx1 = _VtxCurrentIdx; - for (int i1 = 0; i1 < count; i1++) - { - const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; - unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : idx1 + 4; - - // Average normals - ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; - float dmr2 = dm.x * dm.x + dm.y * dm.y; - if (dmr2 > 0.000001f) - { - float scale = 1.0f / dmr2; - if (scale > 100.0f) - scale = 100.0f; - dm *= scale; - } - ImVec2 dm_out = dm * (half_inner_thickness + AA_SIZE); - ImVec2 dm_in = dm * half_inner_thickness; - temp_points[i2 * 4 + 0] = points[i2] + dm_out; - temp_points[i2 * 4 + 1] = points[i2] + dm_in; - temp_points[i2 * 4 + 2] = points[i2] - dm_in; - temp_points[i2 * 4 + 3] = points[i2] - dm_out; - - // Add indexes - _IdxWritePtr[0] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr[1] = (ImDrawIdx) (idx1 + 1); - _IdxWritePtr[2] = (ImDrawIdx) (idx1 + 2); - _IdxWritePtr[3] = (ImDrawIdx) (idx1 + 2); - _IdxWritePtr[4] = (ImDrawIdx) (idx2 + 2); - _IdxWritePtr[5] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr[6] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr[7] = (ImDrawIdx) (idx1 + 1); - _IdxWritePtr[8] = (ImDrawIdx) (idx1 + 0); - _IdxWritePtr[9] = (ImDrawIdx) (idx1 + 0); - _IdxWritePtr[10] = (ImDrawIdx) (idx2 + 0); - _IdxWritePtr[11] = (ImDrawIdx) (idx2 + 1); - _IdxWritePtr[12] = (ImDrawIdx) (idx2 + 2); - _IdxWritePtr[13] = (ImDrawIdx) (idx1 + 2); - _IdxWritePtr[14] = (ImDrawIdx) (idx1 + 3); - _IdxWritePtr[15] = (ImDrawIdx) (idx1 + 3); - _IdxWritePtr[16] = (ImDrawIdx) (idx2 + 3); - _IdxWritePtr[17] = (ImDrawIdx) (idx2 + 2); - _IdxWritePtr += 18; - - idx1 = idx2; - } - - // Add vertexes - for (int i = 0; i < points_count; i++) - { - _VtxWritePtr[0].pos = temp_points[i * 4 + 0]; - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col_trans; - _VtxWritePtr[1].pos = temp_points[i * 4 + 1]; - _VtxWritePtr[1].uv = uv; - _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos = temp_points[i * 4 + 2]; - _VtxWritePtr[2].uv = uv; - _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos = temp_points[i * 4 + 3]; - _VtxWritePtr[3].uv = uv; - _VtxWritePtr[3].col = col_trans; - _VtxWritePtr += 4; - } - } - _VtxCurrentIdx += (ImDrawIdx) vtx_count; - } - else - { - // Non Anti-aliased Stroke - const int idx_count = count * 6; - const int vtx_count = count * 4; // FIXME-OPT: Not sharing edges - PrimReserve(idx_count, vtx_count); - - for (int i1 = 0; i1 < count; i1++) - { - const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; - const ImVec2 &p1 = points[i1]; - const ImVec2 &p2 = points[i2]; - ImVec2 diff = p2 - p1; - diff *= ImInvLength(diff, 1.0f); - - const float dx = diff.x * (thickness * 0.5f); - const float dy = diff.y * (thickness * 0.5f); - _VtxWritePtr[0].pos.x = p1.x + dy; - _VtxWritePtr[0].pos.y = p1.y - dx; - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos.x = p2.x + dy; - _VtxWritePtr[1].pos.y = p2.y - dx; - _VtxWritePtr[1].uv = uv; - _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos.x = p2.x - dy; - _VtxWritePtr[2].pos.y = p2.y + dx; - _VtxWritePtr[2].uv = uv; - _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos.x = p1.x - dy; - _VtxWritePtr[3].pos.y = p1.y + dx; - _VtxWritePtr[3].uv = uv; - _VtxWritePtr[3].col = col; - _VtxWritePtr += 4; - - _IdxWritePtr[0] = (ImDrawIdx) (_VtxCurrentIdx); - _IdxWritePtr[1] = (ImDrawIdx) (_VtxCurrentIdx + 1); - _IdxWritePtr[2] = (ImDrawIdx) (_VtxCurrentIdx + 2); - _IdxWritePtr[3] = (ImDrawIdx) (_VtxCurrentIdx); - _IdxWritePtr[4] = (ImDrawIdx) (_VtxCurrentIdx + 2); - _IdxWritePtr[5] = (ImDrawIdx) (_VtxCurrentIdx + 3); - _IdxWritePtr += 6; - _VtxCurrentIdx += 4; - } - } -} - -void ImDrawList::AddConvexPolyFilled(const ImVec2 *points, const int points_count, ImU32 col) -{ - const ImVec2 uv = _Data->TexUvWhitePixel; - - if (Flags & ImDrawListFlags_AntiAliasedFill) - { - // Anti-aliased Fill - const float AA_SIZE = 1.0f; - const ImU32 col_trans = col & ~IM_COL32_A_MASK; - const int idx_count = (points_count - 2) * 3 + points_count * 6; - const int vtx_count = (points_count * 2); - PrimReserve(idx_count, vtx_count); - - // Add indexes for fill - unsigned int vtx_inner_idx = _VtxCurrentIdx; - unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; - for (int i = 2; i < points_count; i++) - { - _IdxWritePtr[0] = (ImDrawIdx) (vtx_inner_idx); - _IdxWritePtr[1] = (ImDrawIdx) (vtx_inner_idx + ((i - 1) << 1)); - _IdxWritePtr[2] = (ImDrawIdx) (vtx_inner_idx + (i << 1)); - _IdxWritePtr += 3; - } - - // Compute normals - ImVec2 *temp_normals = (ImVec2 *) alloca(points_count * sizeof(ImVec2)); - for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) - { - const ImVec2 &p0 = points[i0]; - const ImVec2 &p1 = points[i1]; - ImVec2 diff = p1 - p0; - diff *= ImInvLength(diff, 1.0f); - temp_normals[i0].x = diff.y; - temp_normals[i0].y = -diff.x; - } - - for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) - { - // Average normals - const ImVec2 &n0 = temp_normals[i0]; - const ImVec2 &n1 = temp_normals[i1]; - ImVec2 dm = (n0 + n1) * 0.5f; - float dmr2 = dm.x * dm.x + dm.y * dm.y; - if (dmr2 > 0.000001f) - { - float scale = 1.0f / dmr2; - if (scale > 100.0f) - scale = 100.0f; - dm *= scale; - } - dm *= AA_SIZE * 0.5f; - - // Add vertices - _VtxWritePtr[0].pos = (points[i1] - dm); - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col; // Inner - _VtxWritePtr[1].pos = (points[i1] + dm); - _VtxWritePtr[1].uv = uv; - _VtxWritePtr[1].col = col_trans; // Outer - _VtxWritePtr += 2; - - // Add indexes for fringes - _IdxWritePtr[0] = (ImDrawIdx) (vtx_inner_idx + (i1 << 1)); - _IdxWritePtr[1] = (ImDrawIdx) (vtx_inner_idx + (i0 << 1)); - _IdxWritePtr[2] = (ImDrawIdx) (vtx_outer_idx + (i0 << 1)); - _IdxWritePtr[3] = (ImDrawIdx) (vtx_outer_idx + (i0 << 1)); - _IdxWritePtr[4] = (ImDrawIdx) (vtx_outer_idx + (i1 << 1)); - _IdxWritePtr[5] = (ImDrawIdx) (vtx_inner_idx + (i1 << 1)); - _IdxWritePtr += 6; - } - _VtxCurrentIdx += (ImDrawIdx) vtx_count; - } - else - { - // Non Anti-aliased Fill - const int idx_count = (points_count - 2) * 3; - const int vtx_count = points_count; - PrimReserve(idx_count, vtx_count); - for (int i = 0; i < vtx_count; i++) - { - _VtxWritePtr[0].pos = points[i]; - _VtxWritePtr[0].uv = uv; - _VtxWritePtr[0].col = col; - _VtxWritePtr++; - } - for (int i = 2; i < points_count; i++) - { - _IdxWritePtr[0] = (ImDrawIdx) (_VtxCurrentIdx); - _IdxWritePtr[1] = (ImDrawIdx) (_VtxCurrentIdx + i - 1); - _IdxWritePtr[2] = (ImDrawIdx) (_VtxCurrentIdx + i); - _IdxWritePtr += 3; - } - _VtxCurrentIdx += (ImDrawIdx) vtx_count; - } -} - -void ImDrawList::PathArcToFast(const ImVec2 ¢re, float radius, int a_min_of_12, int a_max_of_12) -{ - if (radius == 0.0f || a_min_of_12 > a_max_of_12) - { - _Path.push_back(centre); - return; - } - _Path.reserve(_Path.Size + (a_max_of_12 - a_min_of_12 + 1)); - for (int a = a_min_of_12; a <= a_max_of_12; a++) - { - const ImVec2 &c = _Data->CircleVtx12[a % IM_ARRAYSIZE(_Data->CircleVtx12)]; - _Path.push_back(ImVec2(centre.x + c.x * radius, centre.y + c.y * radius)); - } -} - -void ImDrawList::PathArcTo(const ImVec2 ¢re, float radius, float a_min, float a_max, int num_segments) -{ - if (radius == 0.0f) - { - _Path.push_back(centre); - return; - } - _Path.reserve(_Path.Size + (num_segments + 1)); - for (int i = 0; i <= num_segments; i++) - { - const float a = a_min + ((float) i / (float) num_segments) * (a_max - a_min); - _Path.push_back(ImVec2(centre.x + cosf(a) * radius, centre.y + sinf(a) * radius)); - } -} - -static void PathBezierToCasteljau(ImVector *path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) -{ - float dx = x4 - x1; - float dy = y4 - y1; - float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); - float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); - d2 = (d2 >= 0) ? d2 : -d2; - d3 = (d3 >= 0) ? d3 : -d3; - if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) - { - path->push_back(ImVec2(x4, y4)); - } - else if (level < 10) - { - float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; - float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; - float x34 = (x3 + x4) * 0.5f, y34 = (y3 + y4) * 0.5f; - float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; - float x234 = (x23 + x34) * 0.5f, y234 = (y23 + y34) * 0.5f; - float x1234 = (x123 + x234) * 0.5f, y1234 = (y123 + y234) * 0.5f; - - PathBezierToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); - PathBezierToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); - } -} - -void ImDrawList::PathBezierCurveTo(const ImVec2 &p2, const ImVec2 &p3, const ImVec2 &p4, int num_segments) -{ - ImVec2 p1 = _Path.back(); - if (num_segments == 0) - { - // Auto-tessellated - PathBezierToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); - } - else - { - float t_step = 1.0f / (float) num_segments; - for (int i_step = 1; i_step <= num_segments; i_step++) - { - float t = t_step * i_step; - float u = 1.0f - t; - float w1 = u * u * u; - float w2 = 3 * u * u * t; - float w3 = 3 * u * t * t; - float w4 = t * t * t; - _Path.push_back(ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y)); - } - } -} - -void ImDrawList::PathRect(const ImVec2 &a, const ImVec2 &b, float rounding, int rounding_corners) -{ - rounding = ImMin(rounding, fabsf(b.x - a.x) * (((rounding_corners & ImDrawCornerFlags_Top) == ImDrawCornerFlags_Top) || ((rounding_corners & ImDrawCornerFlags_Bot) == ImDrawCornerFlags_Bot) ? 0.5f : 1.0f) - 1.0f); - rounding = ImMin(rounding, fabsf(b.y - a.y) * (((rounding_corners & ImDrawCornerFlags_Left) == ImDrawCornerFlags_Left) || ((rounding_corners & ImDrawCornerFlags_Right) == ImDrawCornerFlags_Right) ? 0.5f : 1.0f) - 1.0f); - - if (rounding <= 0.0f || rounding_corners == 0) - { - PathLineTo(a); - PathLineTo(ImVec2(b.x, a.y)); - PathLineTo(b); - PathLineTo(ImVec2(a.x, b.y)); - } - else - { - const float rounding_tl = (rounding_corners & ImDrawCornerFlags_TopLeft) ? rounding : 0.0f; - const float rounding_tr = (rounding_corners & ImDrawCornerFlags_TopRight) ? rounding : 0.0f; - const float rounding_br = (rounding_corners & ImDrawCornerFlags_BotRight) ? rounding : 0.0f; - const float rounding_bl = (rounding_corners & ImDrawCornerFlags_BotLeft) ? rounding : 0.0f; - PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); - PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); - PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); - PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); - } -} - -void ImDrawList::AddLine(const ImVec2 &a, const ImVec2 &b, ImU32 col, float thickness) -{ - if ((col & IM_COL32_A_MASK) == 0) - return; - PathLineTo(a + ImVec2(0.5f, 0.5f)); - PathLineTo(b + ImVec2(0.5f, 0.5f)); - PathStroke(col, false, thickness); -} - -// a: upper-left, b: lower-right. we don't render 1 px sized rectangles properly. -void ImDrawList::AddRect(const ImVec2 &a, const ImVec2 &b, ImU32 col, float rounding, int rounding_corners_flags, float thickness) -{ - if ((col & IM_COL32_A_MASK) == 0) - return; - if (Flags & ImDrawListFlags_AntiAliasedLines) - PathRect(a + ImVec2(0.5f, 0.5f), b - ImVec2(0.50f, 0.50f), rounding, rounding_corners_flags); - else - PathRect(a + ImVec2(0.5f, 0.5f), b - ImVec2(0.49f, 0.49f), rounding, rounding_corners_flags); // Better looking lower-right corner and rounded non-AA shapes. - PathStroke(col, true, thickness); -} - -void ImDrawList::AddRectFilled(const ImVec2 &a, const ImVec2 &b, ImU32 col, float rounding, int rounding_corners_flags) +// Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) +static unsigned int stb_decompress_length(const unsigned char* input); +static unsigned int stb_decompress(unsigned char* output, const unsigned char* input, unsigned int length); +static const char* GetDefaultCompressedFontDataTTFBase85(); +static unsigned int Decode85Byte(char c) { return c >= '\\' ? c-36 : c-35; } +static void Decode85(const unsigned char* src, unsigned char* dst) { - if ((col & IM_COL32_A_MASK) == 0) - return; - if (rounding > 0.0f) - { - PathRect(a, b, rounding, rounding_corners_flags); - PathFillConvex(col); - } - else - { - PrimReserve(6, 4); - PrimRect(a, b, col); - } + while (*src) + { + unsigned int tmp = Decode85Byte(src[0]) + 85 * (Decode85Byte(src[1]) + 85 * (Decode85Byte(src[2]) + 85 * (Decode85Byte(src[3]) + 85 * Decode85Byte(src[4])))); + dst[0] = ((tmp >> 0) & 0xFF); dst[1] = ((tmp >> 8) & 0xFF); dst[2] = ((tmp >> 16) & 0xFF); dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. + src += 5; + dst += 4; + } } -void ImDrawList::AddRectFilledMultiColor(const ImVec2 &a, const ImVec2 &c, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +// Load embedded ProggyClean.ttf at size 13, disable oversampling +ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) { - if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) - return; - - const ImVec2 uv = _Data->TexUvWhitePixel; - PrimReserve(6, 4); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx)); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx + 1)); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx + 2)); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx)); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx + 2)); - PrimWriteIdx((ImDrawIdx) (_VtxCurrentIdx + 3)); - PrimWriteVtx(a, uv, col_upr_left); - PrimWriteVtx(ImVec2(c.x, a.y), uv, col_upr_right); - PrimWriteVtx(c, uv, col_bot_right); - PrimWriteVtx(ImVec2(a.x, c.y), uv, col_bot_left); + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (!font_cfg_template) + { + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + } + if (font_cfg.SizePixels <= 0.0f) + font_cfg.SizePixels = 13.0f * 1.0f; + if (font_cfg.Name[0] == '\0') + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); + font_cfg.EllipsisChar = (ImWchar)0x0085; + font_cfg.GlyphOffset.y = 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units + + const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); + const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); + ImFont* font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges); + return font; } -void ImDrawList::AddQuad(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col, float thickness) +ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathLineTo(d); - PathStroke(col, true, thickness); + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + size_t data_size = 0; + void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); + if (!data) + { + IM_ASSERT_USER_ERROR(0, "Could not load font file!"); + return NULL; + } + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (font_cfg.Name[0] == '\0') + { + // Store a short copy of filename into into the font name for convenience + const char* p; + for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); + } + return AddFontFromMemoryTTF(data, (int)data_size, size_pixels, &font_cfg, glyph_ranges); } -void ImDrawList::AddQuadFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col) +// NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathLineTo(d); - PathFillConvex(col); + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + IM_ASSERT(font_data_size > 100 && "Incorrect value for font_data_size!"); // Heuristic to prevent accidentally passing a wrong value to font_data_size. + font_cfg.FontData = font_data; + font_cfg.FontDataSize = font_data_size; + font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; + if (glyph_ranges) + font_cfg.GlyphRanges = glyph_ranges; + return AddFont(&font_cfg); } -void ImDrawList::AddTriangle(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col, float thickness) +ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathStroke(col, true, thickness); + const unsigned int buf_decompressed_size = stb_decompress_length((const unsigned char*)compressed_ttf_data); + unsigned char* buf_decompressed_data = (unsigned char*)IM_ALLOC(buf_decompressed_size); + stb_decompress(buf_decompressed_data, (const unsigned char*)compressed_ttf_data, (unsigned int)compressed_ttf_size); + + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontDataOwnedByAtlas = true; + return AddFontFromMemoryTTF(buf_decompressed_data, (int)buf_decompressed_size, size_pixels, &font_cfg, glyph_ranges); } -void ImDrawList::AddTriangleFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col) +ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathFillConvex(col); + int compressed_ttf_size = (((int)strlen(compressed_ttf_data_base85) + 4) / 5) * 4; + void* compressed_ttf = IM_ALLOC((size_t)compressed_ttf_size); + Decode85((const unsigned char*)compressed_ttf_data_base85, (unsigned char*)compressed_ttf); + ImFont* font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); + IM_FREE(compressed_ttf); + return font; } -void ImDrawList::AddCircle(const ImVec2 ¢re, float radius, ImU32 col, int num_segments, float thickness) +int ImFontAtlas::AddCustomRectRegular(int width, int height) { - if ((col & IM_COL32_A_MASK) == 0) - return; + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} - const float a_max = IM_PI * 2.0f * ((float) num_segments - 1.0f) / (float) num_segments; - PathArcTo(centre, radius - 0.5f, 0.0f, a_max, num_segments); - PathStroke(col, true, thickness); +int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) +{ +#ifdef IMGUI_USE_WCHAR32 + IM_ASSERT(id <= IM_UNICODE_CODEPOINT_MAX); +#endif + IM_ASSERT(font != NULL); + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + r.GlyphID = id; + r.GlyphAdvanceX = advance_x; + r.GlyphOffset = offset; + r.Font = font; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index } -void ImDrawList::AddCircleFilled(const ImVec2 ¢re, float radius, ImU32 col, int num_segments) +void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const { - if ((col & IM_COL32_A_MASK) == 0) - return; + IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates + IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed + *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y); + *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); +} - const float a_max = IM_PI * 2.0f * ((float) num_segments - 1.0f) / (float) num_segments; - PathArcTo(centre, radius, 0.0f, a_max, num_segments); - PathFillConvex(col); +bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +{ + if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_COUNT) + return false; + if (Flags & ImFontAtlasFlags_NoMouseCursors) + return false; + + IM_ASSERT(PackIdMouseCursors != -1); + ImFontAtlasCustomRect* r = GetCustomRectByIndex(PackIdMouseCursors); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->X, (float)r->Y); + ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; + *out_size = size; + *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; + out_uv_border[0] = (pos) * TexUvScale; + out_uv_border[1] = (pos + size) * TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + out_uv_fill[0] = (pos) * TexUvScale; + out_uv_fill[1] = (pos + size) * TexUvScale; + return true; } -void ImDrawList::AddBezierCurve(const ImVec2 &pos0, const ImVec2 &cp0, const ImVec2 &cp1, const ImVec2 &pos1, ImU32 col, float thickness, int num_segments) +bool ImFontAtlas::Build() { - if ((col & IM_COL32_A_MASK) == 0) - return; + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + + // Default font is none are specified + if (ConfigData.Size == 0) + AddFontDefault(); + + // Select builder + // - Note that we do not reassign to atlas->FontBuilderIO, since it is likely to point to static data which + // may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are + // using a hot-reloading scheme that messes up static data, store your own instance of ImFontBuilderIO somewhere + // and point to it instead of pointing directly to return value of the GetBuilderXXX functions. + const ImFontBuilderIO* builder_io = FontBuilderIO; + if (builder_io == NULL) + { +#ifdef IMGUI_ENABLE_FREETYPE + builder_io = ImGuiFreeType::GetBuilderForFreeType(); +#elif defined(IMGUI_ENABLE_STB_TRUETYPE) + builder_io = ImFontAtlasGetBuilderForStbTruetype(); +#else + IM_ASSERT(0); // Invalid Build function +#endif + } - PathLineTo(pos0); - PathBezierCurveTo(cp0, cp1, pos1, num_segments); - PathStroke(col, false, thickness); + // Build + return builder_io->FontBuilder_Build(this); } -void ImDrawList::AddText(const ImFont *font, float font_size, const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end, float wrap_width, const ImVec4 *cpu_fine_clip_rect) +void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) { - if ((col & IM_COL32_A_MASK) == 0) - return; + for (unsigned int i = 0; i < 256; i++) + { + unsigned int value = (unsigned int)(i * in_brighten_factor); + out_table[i] = value > 255 ? 255 : (value & 0xFF); + } +} - if (text_end == NULL) - text_end = text_begin + strlen(text_begin); - if (text_begin == text_end) - return; +void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) +{ + IM_ASSERT_PARANOID(w <= stride); + unsigned char* data = pixels + x + y * stride; + for (int j = h; j > 0; j--, data += stride - w) + for (int i = w; i > 0; i--, data++) + *data = table[*data]; +} - // Pull default font/size from the shared ImDrawListSharedData instance - if (font == NULL) - font = _Data->Font; - if (font_size == 0.0f) - font_size = _Data->FontSize; +#ifdef IMGUI_ENABLE_STB_TRUETYPE +// Temporary data for one source font (multiple source fonts can be merged into one destination ImFont) +// (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.) +struct ImFontBuildSrcData +{ + stbtt_fontinfo FontInfo; + stbtt_pack_range PackRange; // Hold the list of codepoints to pack (essentially points to Codepoints.Data) + stbrp_rect* Rects; // Rectangle to pack. We first fill in their size and the packer will give us their position. + stbtt_packedchar* PackedChars; // Output glyphs + const ImWchar* SrcRanges; // Ranges as requested by user (user is allowed to request too much, e.g. 0x0020..0xFFFF) + int DstIndex; // Index into atlas->Fonts[] and dst_tmp_array[] + int GlyphsHighest; // Highest requested codepoint + int GlyphsCount; // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font) + ImBitVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB) + ImVector GlyphsList; // Glyph codepoints list (flattened version of GlyphsSet) +}; - IM_ASSERT(font->ContainerAtlas->TexID == _TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. +// Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont) +struct ImFontBuildDstData +{ + int SrcCount; // Number of source fonts targeting this destination font. + int GlyphsHighest; + int GlyphsCount; + ImBitVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font. +}; - ImVec4 clip_rect = _ClipRectStack.back(); - if (cpu_fine_clip_rect) - { - clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); - clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect->y); - clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); - clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); - } - font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); +static void UnpackBitVectorToFlatIndexList(const ImBitVector* in, ImVector* out) +{ + IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int)); + const ImU32* it_begin = in->Storage.begin(); + const ImU32* it_end = in->Storage.end(); + for (const ImU32* it = it_begin; it < it_end; it++) + if (ImU32 entries_32 = *it) + for (ImU32 bit_n = 0; bit_n < 32; bit_n++) + if (entries_32 & ((ImU32)1 << bit_n)) + out->push_back((int)(((it - it_begin) << 5) + bit_n)); } -void ImDrawList::AddText(const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end) +static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) { - AddText(NULL, 0.0f, pos, col, text_begin, text_end); + IM_ASSERT(atlas->ConfigData.Size > 0); + + ImFontAtlasBuildInit(atlas); + + // Clear atlas + atlas->TexID = (ImTextureID)NULL; + atlas->TexWidth = atlas->TexHeight = 0; + atlas->TexUvScale = ImVec2(0.0f, 0.0f); + atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); + atlas->ClearTexData(); + + // Temporary storage for building + ImVector src_tmp_array; + ImVector dst_tmp_array; + src_tmp_array.resize(atlas->ConfigData.Size); + dst_tmp_array.resize(atlas->Fonts.Size); + memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes()); + memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes()); + + // 1. Initialize font loading structure, check font data validity + for (int src_i = 0; src_i < atlas->ConfigData.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontConfig& cfg = atlas->ConfigData[src_i]; + IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); + + // Find index from cfg.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices) + src_tmp.DstIndex = -1; + for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++) + if (cfg.DstFont == atlas->Fonts[output_i]) + src_tmp.DstIndex = output_i; + if (src_tmp.DstIndex == -1) + { + IM_ASSERT(src_tmp.DstIndex != -1); // cfg.DstFont not pointing within atlas->Fonts[] array? + return false; + } + // Initialize helper structure for font loading and verify that the TTF/OTF data is correct + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); + IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found."); + if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + { + IM_ASSERT(0 && "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize."); + return false; + } + + // Measure highest codepoints + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.SrcRanges = cfg.GlyphRanges ? cfg.GlyphRanges : atlas->GetGlyphRangesDefault(); + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + { + // Check for valid range. This may also help detect *some* dangling pointers, because a common + // user error is to setup ImFontConfig::GlyphRanges with a pointer to data that isn't persistent, + // or to forget to zero-terminate the glyph range array. + IM_ASSERT(src_range[0] <= src_range[1] && "Invalid range: is your glyph range array persistent? it is zero-terminated?"); + src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]); + } + dst_tmp.SrcCount++; + dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest); + } + + // 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs. + int total_glyphs_count = 0; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.GlyphsSet.Create(src_tmp.GlyphsHighest + 1); + if (dst_tmp.GlyphsSet.Storage.empty()) + dst_tmp.GlyphsSet.Create(dst_tmp.GlyphsHighest + 1); + + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + for (unsigned int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++) + { + if (dst_tmp.GlyphsSet.TestBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true) + continue; + if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint)) // It is actually in the font? + continue; + + // Add to avail set/counters + src_tmp.GlyphsCount++; + dst_tmp.GlyphsCount++; + src_tmp.GlyphsSet.SetBit(codepoint); + dst_tmp.GlyphsSet.SetBit(codepoint); + total_glyphs_count++; + } + } + + // 3. Unpack our bit map into a flat list (we now have all the Unicode points that we know are requested _and_ available _and_ not overlapping another) + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount); + UnpackBitVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList); + src_tmp.GlyphsSet.Clear(); + IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount); + } + for (int dst_i = 0; dst_i < dst_tmp_array.Size; dst_i++) + dst_tmp_array[dst_i].GlyphsSet.Clear(); + dst_tmp_array.clear(); + + // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) + // (We technically don't need to zero-clear buf_rects, but let's do it for the sake of sanity) + ImVector buf_rects; + ImVector buf_packedchars; + buf_rects.resize(total_glyphs_count); + buf_packedchars.resize(total_glyphs_count); + memset(buf_rects.Data, 0, (size_t)buf_rects.size_in_bytes()); + memset(buf_packedchars.Data, 0, (size_t)buf_packedchars.size_in_bytes()); + + // 4. Gather glyphs sizes so we can pack them in our virtual canvas. + int total_surface = 0; + int buf_rects_out_n = 0; + int buf_packedchars_out_n = 0; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + src_tmp.Rects = &buf_rects[buf_rects_out_n]; + src_tmp.PackedChars = &buf_packedchars[buf_packedchars_out_n]; + buf_rects_out_n += src_tmp.GlyphsCount; + buf_packedchars_out_n += src_tmp.GlyphsCount; + + // Convert our ranges in the format stb_truetype wants + ImFontConfig& cfg = atlas->ConfigData[src_i]; + src_tmp.PackRange.font_size = cfg.SizePixels * cfg.RasterizerDensity; + src_tmp.PackRange.first_unicode_codepoint_in_range = 0; + src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data; + src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size; + src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars; + src_tmp.PackRange.h_oversample = (unsigned char)cfg.OversampleH; + src_tmp.PackRange.v_oversample = (unsigned char)cfg.OversampleV; + + // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects) + const float scale = (cfg.SizePixels > 0.0f) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels * cfg.RasterizerDensity) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels * cfg.RasterizerDensity); + const int padding = atlas->TexGlyphPadding; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++) + { + int x0, y0, x1, y1; + const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]); + IM_ASSERT(glyph_index_in_font != 0); + stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * cfg.OversampleH, scale * cfg.OversampleV, 0, 0, &x0, &y0, &x1, &y1); + src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1); + src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1); + total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h; + } + } + + // We need a width for the skyline algorithm, any width! + // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. + // User can override TexDesiredWidth and TexGlyphPadding if they wish, otherwise we use a simple heuristic to select the width based on expected surface. + const int surface_sqrt = (int)ImSqrt((float)total_surface) + 1; + atlas->TexHeight = 0; + if (atlas->TexDesiredWidth > 0) + atlas->TexWidth = atlas->TexDesiredWidth; + else + atlas->TexWidth = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + // 5. Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + stbtt_pack_context spc = {}; + stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, atlas->TexGlyphPadding, NULL); + ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); + + // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point. + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbrp_pack_rects((stbrp_context*)spc.pack_info, src_tmp.Rects, src_tmp.GlyphsCount); + + // Extend texture height and mark missing glyphs as non-packed so we won't render them. + // FIXME: We are not handling packing failure here (would happen if we got off TEX_HEIGHT_MAX or if a single if larger than TexWidth?) + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + if (src_tmp.Rects[glyph_i].was_packed) + atlas->TexHeight = ImMax(atlas->TexHeight, src_tmp.Rects[glyph_i].y + src_tmp.Rects[glyph_i].h); + } + + // 7. Allocate texture + atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); + atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = (unsigned char*)IM_ALLOC(atlas->TexWidth * atlas->TexHeight); + memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); + spc.pixels = atlas->TexPixelsAlpha8; + spc.height = atlas->TexHeight; + + // 8. Render/rasterize font characters into the texture + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontConfig& cfg = atlas->ConfigData[src_i]; + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects); + + // Apply multiply operator + if (cfg.RasterizerMultiply != 1.0f) + { + unsigned char multiply_table[256]; + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); + stbrp_rect* r = &src_tmp.Rects[0]; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++) + if (r->was_packed) + ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, atlas->TexPixelsAlpha8, r->x, r->y, r->w, r->h, atlas->TexWidth * 1); + } + src_tmp.Rects = NULL; + } + + // End packing + stbtt_PackEnd(&spc); + buf_rects.clear(); + + // 9. Setup ImFont and glyphs for runtime + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + // When merging fonts with MergeMode=true: + // - We can have multiple input fonts writing into a same destination font. + // - dst_font->ConfigData is != from cfg which is our source configuration. + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontConfig& cfg = atlas->ConfigData[src_i]; + ImFont* dst_font = cfg.DstFont; + + const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels); + int unscaled_ascent, unscaled_descent, unscaled_line_gap; + stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); + + const float ascent = ImCeil(unscaled_ascent * font_scale); + const float descent = ImFloor(unscaled_descent * font_scale); + ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); + const float font_off_x = cfg.GlyphOffset.x; + const float font_off_y = cfg.GlyphOffset.y + IM_ROUND(dst_font->Ascent); + + const float inv_rasterization_scale = 1.0f / cfg.RasterizerDensity; + + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + { + // Register glyph + const int codepoint = src_tmp.GlyphsList[glyph_i]; + const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i]; + stbtt_aligned_quad q; + float unused_x = 0.0f, unused_y = 0.0f; + stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0); + float x0 = q.x0 * inv_rasterization_scale + font_off_x; + float y0 = q.y0 * inv_rasterization_scale + font_off_y; + float x1 = q.x1 * inv_rasterization_scale + font_off_x; + float y1 = q.y1 * inv_rasterization_scale + font_off_y; + dst_font->AddGlyph(&cfg, (ImWchar)codepoint, x0, y0, x1, y1, q.s0, q.t0, q.s1, q.t1, pc.xadvance * inv_rasterization_scale); + } + } + + // Cleanup + src_tmp_array.clear_destruct(); + + ImFontAtlasBuildFinish(atlas); + return true; } -void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, ImU32 col) +const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype() { - if ((col & IM_COL32_A_MASK) == 0) - return; + static ImFontBuilderIO io; + io.FontBuilder_Build = ImFontAtlasBuildWithStbTruetype; + return &io; +} - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); +#endif // IMGUI_ENABLE_STB_TRUETYPE - PrimReserve(6, 4); - PrimRectUV(a, b, uv_a, uv_b, col); +void ImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas) +{ + for (ImFontConfig& font_cfg : atlas->ConfigData) + { + ImFont* font = font_cfg.DstFont; + if (!font_cfg.MergeMode) + { + font->ConfigData = &font_cfg; + font->ConfigDataCount = 0; + } + font->ConfigDataCount++; + } +} - if (push_texture_id) - PopTextureID(); +void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) +{ + if (!font_config->MergeMode) + { + font->ClearOutputData(); + font->FontSize = font_config->SizePixels; + IM_ASSERT(font->ConfigData == font_config); + font->ContainerAtlas = atlas; + font->Ascent = ascent; + font->Descent = descent; + } } -void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, const ImVec2 &uv_a, const ImVec2 &uv_b, const ImVec2 &uv_c, const ImVec2 &uv_d, ImU32 col) +void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque) { - if ((col & IM_COL32_A_MASK) == 0) - return; + stbrp_context* pack_context = (stbrp_context*)stbrp_context_opaque; + IM_ASSERT(pack_context != NULL); - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); + ImVector& user_rects = atlas->CustomRects; + IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. +#ifdef __GNUC__ + if (user_rects.Size < 1) { __builtin_unreachable(); } // Workaround for GCC bug if IM_ASSERT() is defined to conditionally throw (see #5343) +#endif - PrimReserve(6, 4); - PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); + ImVector pack_rects; + pack_rects.resize(user_rects.Size); + memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes()); + for (int i = 0; i < user_rects.Size; i++) + { + pack_rects[i].w = user_rects[i].Width; + pack_rects[i].h = user_rects[i].Height; + } + stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); + for (int i = 0; i < pack_rects.Size; i++) + if (pack_rects[i].was_packed) + { + user_rects[i].X = (unsigned short)pack_rects[i].x; + user_rects[i].Y = (unsigned short)pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); + atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); + } +} - if (push_texture_id) - PopTextureID(); +void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned char* out_pixel = atlas->TexPixelsAlpha8 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : 0x00; } -void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, ImU32 col, float rounding, int rounding_corners) +void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value) { - if ((col & IM_COL32_A_MASK) == 0) - return; + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned int* out_pixel = atlas->TexPixelsRGBA32 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : IM_COL32_BLACK_TRANS; +} - if (rounding <= 0.0f || (rounding_corners & ImDrawCornerFlags_All) == 0) - { - AddImage(user_texture_id, a, b, uv_a, uv_b, col); - return; - } +static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) +{ + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + IM_ASSERT(r->IsPacked()); - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); + const int w = atlas->TexWidth; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + { + // Render/copy pixels + IM_ASSERT(r->Width == FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1 && r->Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + const int x_for_white = r->X; + const int x_for_black = r->X + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + if (atlas->TexPixelsAlpha8 != NULL) + { + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', 0xFF); + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', 0xFF); + } + else + { + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', IM_COL32_WHITE); + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', IM_COL32_WHITE); + } + } + else + { + // Render 4 white pixels + IM_ASSERT(r->Width == 2 && r->Height == 2); + const int offset = (int)r->X + (int)r->Y * w; + if (atlas->TexPixelsAlpha8 != NULL) + { + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + else + { + atlas->TexPixelsRGBA32[offset] = atlas->TexPixelsRGBA32[offset + 1] = atlas->TexPixelsRGBA32[offset + w] = atlas->TexPixelsRGBA32[offset + w + 1] = IM_COL32_WHITE; + } + } + atlas->TexUvWhitePixel = ImVec2((r->X + 0.5f) * atlas->TexUvScale.x, (r->Y + 0.5f) * atlas->TexUvScale.y); +} - int vert_start_idx = VtxBuffer.Size; - PathRect(a, b, rounding, rounding_corners); - PathFillConvex(col); - int vert_end_idx = VtxBuffer.Size; - ImGui::ShadeVertsLinearUV(VtxBuffer.Data + vert_start_idx, VtxBuffer.Data + vert_end_idx, a, b, uv_a, uv_b, true); +static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas) +{ + if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) + return; - if (push_texture_id) - PopTextureID(); + // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdLines); + IM_ASSERT(r->IsPacked()); + for (unsigned int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row + { + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + unsigned int y = n; + unsigned int line_width = n; + unsigned int pad_left = (r->Width - line_width) / 2; + unsigned int pad_right = r->Width - (pad_left + line_width); + + // Write each slice + IM_ASSERT(pad_left + line_width + pad_right == r->Width && y < r->Height); // Make sure we're inside the texture bounds before we start writing pixels + if (atlas->TexPixelsAlpha8 != NULL) + { + unsigned char* write_ptr = &atlas->TexPixelsAlpha8[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (unsigned int i = 0; i < pad_left; i++) + *(write_ptr + i) = 0x00; + + for (unsigned int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = 0xFF; + + for (unsigned int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = 0x00; + } + else + { + unsigned int* write_ptr = &atlas->TexPixelsRGBA32[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (unsigned int i = 0; i < pad_left; i++) + *(write_ptr + i) = IM_COL32(255, 255, 255, 0); + + for (unsigned int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = IM_COL32_WHITE; + + for (unsigned int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0); + } + + // Calculate UVs for this line + ImVec2 uv0 = ImVec2((float)(r->X + pad_left - 1), (float)(r->Y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r->X + pad_left + line_width + 1), (float)(r->Y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); + } } -//----------------------------------------------------------------------------- -// ImDrawData -//----------------------------------------------------------------------------- - -// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! -void ImDrawData::DeIndexAllBuffers() +// Note: this is called / shared by both the stb_truetype and the FreeType builder +void ImFontAtlasBuildInit(ImFontAtlas* atlas) { - ImVector new_vtx_buffer; - TotalVtxCount = TotalIdxCount = 0; - for (int i = 0; i < CmdListsCount; i++) - { - ImDrawList *cmd_list = CmdLists[i]; - if (cmd_list->IdxBuffer.empty()) - continue; - new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); - for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) - new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; - cmd_list->VtxBuffer.swap(new_vtx_buffer); - cmd_list->IdxBuffer.resize(0); - TotalVtxCount += cmd_list->VtxBuffer.Size; - } -} - -// Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. -void ImDrawData::ScaleClipRects(const ImVec2 &scale) -{ - for (int i = 0; i < CmdListsCount; i++) - { - ImDrawList *cmd_list = CmdLists[i]; - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) - { - ImDrawCmd *cmd = &cmd_list->CmdBuffer[cmd_i]; - cmd->ClipRect = ImVec4(cmd->ClipRect.x * scale.x, cmd->ClipRect.y * scale.y, cmd->ClipRect.z * scale.x, cmd->ClipRect.w * scale.y); - } - } + // Round font size + // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet. + // - Note that using io.FontGlobalScale or SetWindowFontScale(), with are legacy-ish, partially supported features, can still lead to unrounded sizes. + // - We may support it better later and remove this rounding. + for (ImFontConfig& cfg : atlas->ConfigData) + cfg.SizePixels = ImTrunc(cfg.SizePixels); + + // Register texture region for mouse cursors or standard white pixels + if (atlas->PackIdMouseCursors < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(2, 2); + } + + // Register texture region for thick lines + // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row + if (atlas->PackIdLines < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoBakedLines)) + atlas->PackIdLines = atlas->AddCustomRectRegular(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1); + } } -//----------------------------------------------------------------------------- -// Shade functions -//----------------------------------------------------------------------------- +// This is called/shared by both the stb_truetype and the FreeType builder. +void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +{ + // Render into our custom data blocks + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL || atlas->TexPixelsRGBA32 != NULL); + ImFontAtlasBuildRenderDefaultTexData(atlas); + ImFontAtlasBuildRenderLinesTexData(atlas); -// Generic linear color gradient, write to RGB fields, leave A untouched. -void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawVert *vert_start, ImDrawVert *vert_end, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) -{ - ImVec2 gradient_extent = gradient_p1 - gradient_p0; - float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); - for (ImDrawVert *vert = vert_start; vert < vert_end; vert++) - { - float d = ImDot(vert->pos - gradient_p0, gradient_extent); - float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); - int r = ImLerp((int) (col0 >> IM_COL32_R_SHIFT) & 0xFF, (int) (col1 >> IM_COL32_R_SHIFT) & 0xFF, t); - int g = ImLerp((int) (col0 >> IM_COL32_G_SHIFT) & 0xFF, (int) (col1 >> IM_COL32_G_SHIFT) & 0xFF, t); - int b = ImLerp((int) (col0 >> IM_COL32_B_SHIFT) & 0xFF, (int) (col1 >> IM_COL32_B_SHIFT) & 0xFF, t); - vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); - } -} - -// Scan and shade backward from the end of given vertices. Assume vertices are text only (= vert_start..vert_end going left to right) so we can break as soon as we are out the gradient bounds. -void ImGui::ShadeVertsLinearAlphaGradientForLeftToRightText(ImDrawVert *vert_start, ImDrawVert *vert_end, float gradient_p0_x, float gradient_p1_x) -{ - float gradient_extent_x = gradient_p1_x - gradient_p0_x; - float gradient_inv_length2 = 1.0f / (gradient_extent_x * gradient_extent_x); - int full_alpha_count = 0; - for (ImDrawVert *vert = vert_end - 1; vert >= vert_start; vert--) - { - float d = (vert->pos.x - gradient_p0_x) * (gradient_extent_x); - float alpha_mul = 1.0f - ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); - if (alpha_mul >= 1.0f && ++full_alpha_count > 2) - return; // Early out - int a = (int) (((vert->col >> IM_COL32_A_SHIFT) & 0xFF) * alpha_mul); - vert->col = (vert->col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); - } + // Register custom rectangle glyphs + for (int i = 0; i < atlas->CustomRects.Size; i++) + { + const ImFontAtlasCustomRect* r = &atlas->CustomRects[i]; + if (r->Font == NULL || r->GlyphID == 0) + continue; + + // Will ignore ImFontConfig settings: GlyphMinAdvanceX, GlyphMinAdvanceY, GlyphExtraSpacing, PixelSnapH + IM_ASSERT(r->Font->ContainerAtlas == atlas); + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(r, &uv0, &uv1); + r->Font->AddGlyph(NULL, (ImWchar)r->GlyphID, r->GlyphOffset.x, r->GlyphOffset.y, r->GlyphOffset.x + r->Width, r->GlyphOffset.y + r->Height, uv0.x, uv0.y, uv1.x, uv1.y, r->GlyphAdvanceX); + } + + // Build all fonts lookup tables + for (ImFont* font : atlas->Fonts) + if (font->DirtyLookupTables) + font->BuildLookupTable(); + + atlas->TexReady = true; } -// Distribute UV over (a, b) rectangle -void ImGui::ShadeVertsLinearUV(ImDrawVert *vert_start, ImDrawVert *vert_end, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, bool clamp) +// Retrieve list of range (2 int per range, values are inclusive) +const ImWchar* ImFontAtlas::GetGlyphRangesDefault() { - const ImVec2 size = b - a; - const ImVec2 uv_size = uv_b - uv_a; - const ImVec2 scale = ImVec2( - size.x != 0.0f ? (uv_size.x / size.x) : 0.0f, - size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; +} - if (clamp) - { - const ImVec2 min = ImMin(uv_a, uv_b); - const ImVec2 max = ImMax(uv_a, uv_b); +const ImWchar* ImFontAtlas::GetGlyphRangesGreek() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0370, 0x03FF, // Greek and Coptic + 0, + }; + return &ranges[0]; +} - for (ImDrawVert *vertex = vert_start; vertex < vert_end; ++vertex) - vertex->uv = ImClamp(uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale), min, max); - } - else - { - for (ImDrawVert *vertex = vert_start; vertex < vert_end; ++vertex) - vertex->uv = uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale); - } +const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3131, 0x3163, // Korean alphabets + 0xAC00, 0xD7A3, // Korean characters + 0xFFFD, 0xFFFD, // Invalid + 0, + }; + return &ranges[0]; } -//----------------------------------------------------------------------------- -// ImFontConfig -//----------------------------------------------------------------------------- +const ImWchar* ImFontAtlas::GetGlyphRangesChineseFull() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD, // Invalid + 0x4e00, 0x9FAF, // CJK Ideograms + 0, + }; + return &ranges[0]; +} -ImFontConfig::ImFontConfig() +static void UnpackAccumulativeOffsetsIntoRanges(int base_codepoint, const short* accumulative_offsets, int accumulative_offsets_count, ImWchar* out_ranges) { - FontData = NULL; - FontDataSize = 0; - FontDataOwnedByAtlas = true; - FontNo = 0; - SizePixels = 0.0f; - OversampleH = 3; - OversampleV = 1; - PixelSnapH = false; - GlyphExtraSpacing = ImVec2(0.0f, 0.0f); - GlyphOffset = ImVec2(0.0f, 0.0f); - GlyphRanges = NULL; - MergeMode = false; - RasterizerFlags = 0x00; - RasterizerMultiply = 1.0f; - memset(Name, 0, sizeof(Name)); - DstFont = NULL; + for (int n = 0; n < accumulative_offsets_count; n++, out_ranges += 2) + { + out_ranges[0] = out_ranges[1] = (ImWchar)(base_codepoint + accumulative_offsets[n]); + base_codepoint += accumulative_offsets[n]; + } + out_ranges[0] = 0; } -//----------------------------------------------------------------------------- -// ImFontAtlas -//----------------------------------------------------------------------------- +//------------------------------------------------------------------------- +// [SECTION] ImFontAtlas glyph ranges helpers +//------------------------------------------------------------------------- -// A work of art lies ahead! (. = white layer, X = black layer, others are blank) -// The white texels on the top left are the ones we'll use everywhere in ImGui to render filled shapes. -const int FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF = 90; -const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; -const unsigned int FONT_ATLAS_DEFAULT_TEX_DATA_ID = 0x80000000; -static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() +{ + // Store 2500 regularly used characters for Simplified Chinese. + // Sourced from https://zh.wiktionary.org/wiki/%E9%99%84%E5%BD%95:%E7%8E%B0%E4%BB%A3%E6%B1%89%E8%AF%AD%E5%B8%B8%E7%94%A8%E5%AD%97%E8%A1%A8 + // This table covers 97.97% of all characters used during the month in July, 1987. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = { - "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX" - "..- -X.....X- X.X - X.X -X.....X - X.....X" - "--- -XXX.XXX- X...X - X...X -X....X - X....X" - "X - X.X - X.....X - X.....X -X...X - X...X" - "XX - X.X -X.......X- X.......X -X..X.X - X.X..X" - "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X" - "X..X - X.X - X.X - X.X -XX X.X - X.X XX" - "X...X - X.X - X.X - XX X.X XX - X.X - X.X " - "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X " - "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X " - "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X " - "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X " - "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X " - "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X " - "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X " - "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X " - "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX " - "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------" - "X.X X..X - -X.......X- X.......X - XX XX - " - "XX X..X - - X.....X - X.....X - X.X X.X - " - " X..X - X...X - X...X - X..X X..X - " - " XX - X.X - X.X - X...XXXXXXXXXXXXX...X - " - "------------ - X - X -X.....................X- " - " ----------------------------------- X...XXXXXXXXXXXXX...X - " - " - X..X X..X - " - " - X.X X.X - " - " - XX XX - "}; - -static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_Count_][3] = + 0,1,2,4,1,1,1,1,2,1,3,2,1,2,2,1,1,1,1,1,5,2,1,2,3,3,3,2,2,4,1,1,1,2,1,5,2,3,1,2,1,2,1,1,2,1,1,2,2,1,4,1,1,1,1,5,10,1,2,19,2,1,2,1,2,1,2,1,2, + 1,5,1,6,3,2,1,2,2,1,1,1,4,8,5,1,1,4,1,1,3,1,2,1,5,1,2,1,1,1,10,1,1,5,2,4,6,1,4,2,2,2,12,2,1,1,6,1,1,1,4,1,1,4,6,5,1,4,2,2,4,10,7,1,1,4,2,4, + 2,1,4,3,6,10,12,5,7,2,14,2,9,1,1,6,7,10,4,7,13,1,5,4,8,4,1,1,2,28,5,6,1,1,5,2,5,20,2,2,9,8,11,2,9,17,1,8,6,8,27,4,6,9,20,11,27,6,68,2,2,1,1, + 1,2,1,2,2,7,6,11,3,3,1,1,3,1,2,1,1,1,1,1,3,1,1,8,3,4,1,5,7,2,1,4,4,8,4,2,1,2,1,1,4,5,6,3,6,2,12,3,1,3,9,2,4,3,4,1,5,3,3,1,3,7,1,5,1,1,1,1,2, + 3,4,5,2,3,2,6,1,1,2,1,7,1,7,3,4,5,15,2,2,1,5,3,22,19,2,1,1,1,1,2,5,1,1,1,6,1,1,12,8,2,9,18,22,4,1,1,5,1,16,1,2,7,10,15,1,1,6,2,4,1,2,4,1,6, + 1,1,3,2,4,1,6,4,5,1,2,1,1,2,1,10,3,1,3,2,1,9,3,2,5,7,2,19,4,3,6,1,1,1,1,1,4,3,2,1,1,1,2,5,3,1,1,1,2,2,1,1,2,1,1,2,1,3,1,1,1,3,7,1,4,1,1,2,1, + 1,2,1,2,4,4,3,8,1,1,1,2,1,3,5,1,3,1,3,4,6,2,2,14,4,6,6,11,9,1,15,3,1,28,5,2,5,5,3,1,3,4,5,4,6,14,3,2,3,5,21,2,7,20,10,1,2,19,2,4,28,28,2,3, + 2,1,14,4,1,26,28,42,12,40,3,52,79,5,14,17,3,2,2,11,3,4,6,3,1,8,2,23,4,5,8,10,4,2,7,3,5,1,1,6,3,1,2,2,2,5,28,1,1,7,7,20,5,3,29,3,17,26,1,8,4, + 27,3,6,11,23,5,3,4,6,13,24,16,6,5,10,25,35,7,3,2,3,3,14,3,6,2,6,1,4,2,3,8,2,1,1,3,3,3,4,1,1,13,2,2,4,5,2,1,14,14,1,2,2,1,4,5,2,3,1,14,3,12, + 3,17,2,16,5,1,2,1,8,9,3,19,4,2,2,4,17,25,21,20,28,75,1,10,29,103,4,1,2,1,1,4,2,4,1,2,3,24,2,2,2,1,1,2,1,3,8,1,1,1,2,1,1,3,1,1,1,6,1,5,3,1,1, + 1,3,4,1,1,5,2,1,5,6,13,9,16,1,1,1,1,3,2,3,2,4,5,2,5,2,2,3,7,13,7,2,2,1,1,1,1,2,3,3,2,1,6,4,9,2,1,14,2,14,2,1,18,3,4,14,4,11,41,15,23,15,23, + 176,1,3,4,1,1,1,1,5,3,1,2,3,7,3,1,1,2,1,2,4,4,6,2,4,1,9,7,1,10,5,8,16,29,1,1,2,2,3,1,3,5,2,4,5,4,1,1,2,2,3,3,7,1,6,10,1,17,1,44,4,6,2,1,1,6, + 5,4,2,10,1,6,9,2,8,1,24,1,2,13,7,8,8,2,1,4,1,3,1,3,3,5,2,5,10,9,4,9,12,2,1,6,1,10,1,1,7,7,4,10,8,3,1,13,4,3,1,6,1,3,5,2,1,2,17,16,5,2,16,6, + 1,4,2,1,3,3,6,8,5,11,11,1,3,3,2,4,6,10,9,5,7,4,7,4,7,1,1,4,2,1,3,6,8,7,1,6,11,5,5,3,24,9,4,2,7,13,5,1,8,82,16,61,1,1,1,4,2,2,16,10,3,8,1,1, + 6,4,2,1,3,1,1,1,4,3,8,4,2,2,1,1,1,1,1,6,3,5,1,1,4,6,9,2,1,1,1,2,1,7,2,1,6,1,5,4,4,3,1,8,1,3,3,1,3,2,2,2,2,3,1,6,1,2,1,2,1,3,7,1,8,2,1,2,1,5, + 2,5,3,5,10,1,2,1,1,3,2,5,11,3,9,3,5,1,1,5,9,1,2,1,5,7,9,9,8,1,3,3,3,6,8,2,3,2,1,1,32,6,1,2,15,9,3,7,13,1,3,10,13,2,14,1,13,10,2,1,3,10,4,15, + 2,15,15,10,1,3,9,6,9,32,25,26,47,7,3,2,3,1,6,3,4,3,2,8,5,4,1,9,4,2,2,19,10,6,2,3,8,1,2,2,4,2,1,9,4,4,4,6,4,8,9,2,3,1,1,1,1,3,5,5,1,3,8,4,6, + 2,1,4,12,1,5,3,7,13,2,5,8,1,6,1,2,5,14,6,1,5,2,4,8,15,5,1,23,6,62,2,10,1,1,8,1,2,2,10,4,2,2,9,2,1,1,3,2,3,1,5,3,3,2,1,3,8,1,1,1,11,3,1,1,4, + 3,7,1,14,1,2,3,12,5,2,5,1,6,7,5,7,14,11,1,3,1,8,9,12,2,1,11,8,4,4,2,6,10,9,13,1,1,3,1,5,1,3,2,4,4,1,18,2,3,14,11,4,29,4,2,7,1,3,13,9,2,2,5, + 3,5,20,7,16,8,5,72,34,6,4,22,12,12,28,45,36,9,7,39,9,191,1,1,1,4,11,8,4,9,2,3,22,1,1,1,1,4,17,1,7,7,1,11,31,10,2,4,8,2,3,2,1,4,2,16,4,32,2, + 3,19,13,4,9,1,5,2,14,8,1,1,3,6,19,6,5,1,16,6,2,10,8,5,1,2,3,1,5,5,1,11,6,6,1,3,3,2,6,3,8,1,1,4,10,7,5,7,7,5,8,9,2,1,3,4,1,1,3,1,3,3,2,6,16, + 1,4,6,3,1,10,6,1,3,15,2,9,2,10,25,13,9,16,6,2,2,10,11,4,3,9,1,2,6,6,5,4,30,40,1,10,7,12,14,33,6,3,6,7,3,1,3,1,11,14,4,9,5,12,11,49,18,51,31, + 140,31,2,2,1,5,1,8,1,10,1,4,4,3,24,1,10,1,3,6,6,16,3,4,5,2,1,4,2,57,10,6,22,2,22,3,7,22,6,10,11,36,18,16,33,36,2,5,5,1,1,1,4,10,1,4,13,2,7, + 5,2,9,3,4,1,7,43,3,7,3,9,14,7,9,1,11,1,1,3,7,4,18,13,1,14,1,3,6,10,73,2,2,30,6,1,11,18,19,13,22,3,46,42,37,89,7,3,16,34,2,2,3,9,1,7,1,1,1,2, + 2,4,10,7,3,10,3,9,5,28,9,2,6,13,7,3,1,3,10,2,7,2,11,3,6,21,54,85,2,1,4,2,2,1,39,3,21,2,2,5,1,1,1,4,1,1,3,4,15,1,3,2,4,4,2,3,8,2,20,1,8,7,13, + 4,1,26,6,2,9,34,4,21,52,10,4,4,1,5,12,2,11,1,7,2,30,12,44,2,30,1,1,3,6,16,9,17,39,82,2,2,24,7,1,7,3,16,9,14,44,2,1,2,1,2,3,5,2,4,1,6,7,5,3, + 2,6,1,11,5,11,2,1,18,19,8,1,3,24,29,2,1,3,5,2,2,1,13,6,5,1,46,11,3,5,1,1,5,8,2,10,6,12,6,3,7,11,2,4,16,13,2,5,1,1,2,2,5,2,28,5,2,23,10,8,4, + 4,22,39,95,38,8,14,9,5,1,13,5,4,3,13,12,11,1,9,1,27,37,2,5,4,4,63,211,95,2,2,2,1,3,5,2,1,1,2,2,1,1,1,3,2,4,1,2,1,1,5,2,2,1,1,2,3,1,3,1,1,1, + 3,1,4,2,1,3,6,1,1,3,7,15,5,3,2,5,3,9,11,4,2,22,1,6,3,8,7,1,4,28,4,16,3,3,25,4,4,27,27,1,4,1,2,2,7,1,3,5,2,28,8,2,14,1,8,6,16,25,3,3,3,14,3, + 3,1,1,2,1,4,6,3,8,4,1,1,1,2,3,6,10,6,2,3,18,3,2,5,5,4,3,1,5,2,5,4,23,7,6,12,6,4,17,11,9,5,1,1,10,5,12,1,1,11,26,33,7,3,6,1,17,7,1,5,12,1,11, + 2,4,1,8,14,17,23,1,2,1,7,8,16,11,9,6,5,2,6,4,16,2,8,14,1,11,8,9,1,1,1,9,25,4,11,19,7,2,15,2,12,8,52,7,5,19,2,16,4,36,8,1,16,8,24,26,4,6,2,9, + 5,4,36,3,28,12,25,15,37,27,17,12,59,38,5,32,127,1,2,9,17,14,4,1,2,1,1,8,11,50,4,14,2,19,16,4,17,5,4,5,26,12,45,2,23,45,104,30,12,8,3,10,2,2, + 3,3,1,4,20,7,2,9,6,15,2,20,1,3,16,4,11,15,6,134,2,5,59,1,2,2,2,1,9,17,3,26,137,10,211,59,1,2,4,1,4,1,1,1,2,6,2,3,1,1,2,3,2,3,1,3,4,4,2,3,3, + 1,4,3,1,7,2,2,3,1,2,1,3,3,3,2,2,3,2,1,3,14,6,1,3,2,9,6,15,27,9,34,145,1,1,2,1,1,1,1,2,1,1,1,1,2,2,2,3,1,2,1,1,1,2,3,5,8,3,5,2,4,1,3,2,2,2,12, + 4,1,1,1,10,4,5,1,20,4,16,1,15,9,5,12,2,9,2,5,4,2,26,19,7,1,26,4,30,12,15,42,1,6,8,172,1,1,4,2,1,1,11,2,2,4,2,1,2,1,10,8,1,2,1,4,5,1,2,5,1,8, + 4,1,3,4,2,1,6,2,1,3,4,1,2,1,1,1,1,12,5,7,2,4,3,1,1,1,3,3,6,1,2,2,3,3,3,2,1,2,12,14,11,6,6,4,12,2,8,1,7,10,1,35,7,4,13,15,4,3,23,21,28,52,5, + 26,5,6,1,7,10,2,7,53,3,2,1,1,1,2,163,532,1,10,11,1,3,3,4,8,2,8,6,2,2,23,22,4,2,2,4,2,1,3,1,3,3,5,9,8,2,1,2,8,1,10,2,12,21,20,15,105,2,3,1,1, + 3,2,3,1,1,2,5,1,4,15,11,19,1,1,1,1,5,4,5,1,1,2,5,3,5,12,1,2,5,1,11,1,1,15,9,1,4,5,3,26,8,2,1,3,1,1,15,19,2,12,1,2,5,2,7,2,19,2,20,6,26,7,5, + 2,2,7,34,21,13,70,2,128,1,1,2,1,1,2,1,1,3,2,2,2,15,1,4,1,3,4,42,10,6,1,49,85,8,1,2,1,1,4,4,2,3,6,1,5,7,4,3,211,4,1,2,1,2,5,1,2,4,2,2,6,5,6, + 10,3,4,48,100,6,2,16,296,5,27,387,2,2,3,7,16,8,5,38,15,39,21,9,10,3,7,59,13,27,21,47,5,21,6 + }; + static ImWchar base_ranges[] = // not zero-terminated { - // Pos ........ Size ......... Offset ...... - {ImVec2(0, 3), ImVec2(12, 19), ImVec2(0, 0)}, // ImGuiMouseCursor_Arrow - {ImVec2(13, 0), ImVec2(7, 16), ImVec2(4, 8)}, // ImGuiMouseCursor_TextInput - {ImVec2(31, 0), ImVec2(23, 23), ImVec2(11, 11)}, // ImGuiMouseCursor_ResizeAll - {ImVec2(21, 0), ImVec2(9, 23), ImVec2(5, 11)}, // ImGuiMouseCursor_ResizeNS - {ImVec2(55, 18), ImVec2(23, 9), ImVec2(11, 5)}, // ImGuiMouseCursor_ResizeEW - {ImVec2(73, 0), ImVec2(17, 17), ImVec2(9, 9)}, // ImGuiMouseCursor_ResizeNESW - {ImVec2(55, 0), ImVec2(17, 17), ImVec2(9, 9)}, // ImGuiMouseCursor_ResizeNWSE -}; + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00) * 2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; +} -ImFontAtlas::ImFontAtlas() +const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() { - Flags = 0x00; - TexID = NULL; - TexDesiredWidth = 0; - TexGlyphPadding = 1; - - TexPixelsAlpha8 = NULL; - TexPixelsRGBA32 = NULL; - TexWidth = TexHeight = 0; - TexUvScale = ImVec2(0.0f, 0.0f); - TexUvWhitePixel = ImVec2(0.0f, 0.0f); - for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) - CustomRectIds[n] = -1; + // 2999 ideograms code points for Japanese + // - 2136 Joyo (meaning "for regular use" or "for common use") Kanji code points + // - 863 Jinmeiyo (meaning "for personal name") Kanji code points + // - Sourced from official information provided by the government agencies of Japan: + // - List of Joyo Kanji by the Agency for Cultural Affairs + // - https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kijun/naikaku/kanji/ + // - List of Jinmeiyo Kanji by the Ministry of Justice + // - http://www.moj.go.jp/MINJI/minji86.html + // - Available under the terms of the Creative Commons Attribution 4.0 International (CC BY 4.0). + // - https://creativecommons.org/licenses/by/4.0/legalcode + // - You can generate this code by the script at: + // - https://github.com/vaiorabbit/everyday_use_kanji + // - References: + // - List of Joyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/List_of_j%C5%8Dy%C5%8D_kanji + // - List of Jinmeiyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/Jinmeiy%C5%8D_kanji + // - Missing 1 Joyo Kanji: U+20B9F (Kun'yomi: Shikaru, On'yomi: Shitsu,shichi), see https://github.com/ocornut/imgui/pull/3627 for details. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = + { + 0,1,2,4,1,1,1,1,2,1,3,3,2,2,1,5,3,5,7,5,6,1,2,1,7,2,6,3,1,8,1,1,4,1,1,18,2,11,2,6,2,1,2,1,5,1,2,1,3,1,2,1,2,3,3,1,1,2,3,1,1,1,12,7,9,1,4,5,1, + 1,2,1,10,1,1,9,2,2,4,5,6,9,3,1,1,1,1,9,3,18,5,2,2,2,2,1,6,3,7,1,1,1,1,2,2,4,2,1,23,2,10,4,3,5,2,4,10,2,4,13,1,6,1,9,3,1,1,6,6,7,6,3,1,2,11,3, + 2,2,3,2,15,2,2,5,4,3,6,4,1,2,5,2,12,16,6,13,9,13,2,1,1,7,16,4,7,1,19,1,5,1,2,2,7,7,8,2,6,5,4,9,18,7,4,5,9,13,11,8,15,2,1,1,1,2,1,2,2,1,2,2,8, + 2,9,3,3,1,1,4,4,1,1,1,4,9,1,4,3,5,5,2,7,5,3,4,8,2,1,13,2,3,3,1,14,1,1,4,5,1,3,6,1,5,2,1,1,3,3,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1,1,1,1,12,3,3,9,5, + 2,6,1,5,6,1,2,3,18,2,4,14,4,1,3,6,1,1,6,3,5,5,3,2,2,2,2,12,3,1,4,2,3,2,3,11,1,7,4,1,2,1,3,17,1,9,1,24,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,2,4,15,1, + 1,2,1,1,2,1,5,2,5,20,2,5,9,1,10,8,7,6,1,1,1,1,1,1,6,2,1,2,8,1,1,1,1,5,1,1,3,1,1,1,1,3,1,1,12,4,1,3,1,1,1,1,1,10,3,1,7,5,13,1,2,3,4,6,1,1,30, + 2,9,9,1,15,38,11,3,1,8,24,7,1,9,8,10,2,1,9,31,2,13,6,2,9,4,49,5,2,15,2,1,10,2,1,1,1,2,2,6,15,30,35,3,14,18,8,1,16,10,28,12,19,45,38,1,3,2,3, + 13,2,1,7,3,6,5,3,4,3,1,5,7,8,1,5,3,18,5,3,6,1,21,4,24,9,24,40,3,14,3,21,3,2,1,2,4,2,3,1,15,15,6,5,1,1,3,1,5,6,1,9,7,3,3,2,1,4,3,8,21,5,16,4, + 5,2,10,11,11,3,6,3,2,9,3,6,13,1,2,1,1,1,1,11,12,6,6,1,4,2,6,5,2,1,1,3,3,6,13,3,1,1,5,1,2,3,3,14,2,1,2,2,2,5,1,9,5,1,1,6,12,3,12,3,4,13,2,14, + 2,8,1,17,5,1,16,4,2,2,21,8,9,6,23,20,12,25,19,9,38,8,3,21,40,25,33,13,4,3,1,4,1,2,4,1,2,5,26,2,1,1,2,1,3,6,2,1,1,1,1,1,1,2,3,1,1,1,9,2,3,1,1, + 1,3,6,3,2,1,1,6,6,1,8,2,2,2,1,4,1,2,3,2,7,3,2,4,1,2,1,2,2,1,1,1,1,1,3,1,2,5,4,10,9,4,9,1,1,1,1,1,1,5,3,2,1,6,4,9,6,1,10,2,31,17,8,3,7,5,40,1, + 7,7,1,6,5,2,10,7,8,4,15,39,25,6,28,47,18,10,7,1,3,1,1,2,1,1,1,3,3,3,1,1,1,3,4,2,1,4,1,3,6,10,7,8,6,2,2,1,3,3,2,5,8,7,9,12,2,15,1,1,4,1,2,1,1, + 1,3,2,1,3,3,5,6,2,3,2,10,1,4,2,8,1,1,1,11,6,1,21,4,16,3,1,3,1,4,2,3,6,5,1,3,1,1,3,3,4,6,1,1,10,4,2,7,10,4,7,4,2,9,4,3,1,1,1,4,1,8,3,4,1,3,1, + 6,1,4,2,1,4,7,2,1,8,1,4,5,1,1,2,2,4,6,2,7,1,10,1,1,3,4,11,10,8,21,4,6,1,3,5,2,1,2,28,5,5,2,3,13,1,2,3,1,4,2,1,5,20,3,8,11,1,3,3,3,1,8,10,9,2, + 10,9,2,3,1,1,2,4,1,8,3,6,1,7,8,6,11,1,4,29,8,4,3,1,2,7,13,1,4,1,6,2,6,12,12,2,20,3,2,3,6,4,8,9,2,7,34,5,1,18,6,1,1,4,4,5,7,9,1,2,2,4,3,4,1,7, + 2,2,2,6,2,3,25,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,5,3,4,4,3,2,1,1,4,1,2,1,1,3,1,11,1,6,3,1,7,3,6,2,8,8,6,9,3,4,11,3,2,10,12,2,5,11,1,6,4,5, + 3,1,8,5,4,6,6,3,5,1,1,3,2,1,2,2,6,17,12,1,10,1,6,12,1,6,6,19,9,6,16,1,13,4,4,15,7,17,6,11,9,15,12,6,7,2,1,2,2,15,9,3,21,4,6,49,18,7,3,2,3,1, + 6,8,2,2,6,2,9,1,3,6,4,4,1,2,16,2,5,2,1,6,2,3,5,3,1,2,5,1,2,1,9,3,1,8,6,4,8,11,3,1,1,1,1,3,1,13,8,4,1,3,2,2,1,4,1,11,1,5,2,1,5,2,5,8,6,1,1,7, + 4,3,8,3,2,7,2,1,5,1,5,2,4,7,6,2,8,5,1,11,4,5,3,6,18,1,2,13,3,3,1,21,1,1,4,1,4,1,1,1,8,1,2,2,7,1,2,4,2,2,9,2,1,1,1,4,3,6,3,12,5,1,1,1,5,6,3,2, + 4,8,2,2,4,2,7,1,8,9,5,2,3,2,1,3,2,13,7,14,6,5,1,1,2,1,4,2,23,2,1,1,6,3,1,4,1,15,3,1,7,3,9,14,1,3,1,4,1,1,5,8,1,3,8,3,8,15,11,4,14,4,4,2,5,5, + 1,7,1,6,14,7,7,8,5,15,4,8,6,5,6,2,1,13,1,20,15,11,9,2,5,6,2,11,2,6,2,5,1,5,8,4,13,19,25,4,1,1,11,1,34,2,5,9,14,6,2,2,6,1,1,14,1,3,14,13,1,6, + 12,21,14,14,6,32,17,8,32,9,28,1,2,4,11,8,3,1,14,2,5,15,1,1,1,1,3,6,4,1,3,4,11,3,1,1,11,30,1,5,1,4,1,5,8,1,1,3,2,4,3,17,35,2,6,12,17,3,1,6,2, + 1,1,12,2,7,3,3,2,1,16,2,8,3,6,5,4,7,3,3,8,1,9,8,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,4,3,7,5,8,3,3,3,3,3,3,1,23,10,3,1,2,2,6,3,1,16,1,16, + 22,3,10,4,11,6,9,7,7,3,6,2,2,2,4,10,2,1,1,2,8,7,1,6,4,1,3,3,3,5,10,12,12,2,3,12,8,15,1,1,16,6,6,1,5,9,11,4,11,4,2,6,12,1,17,5,13,1,4,9,5,1,11, + 2,1,8,1,5,7,28,8,3,5,10,2,17,3,38,22,1,2,18,12,10,4,38,18,1,4,44,19,4,1,8,4,1,12,1,4,31,12,1,14,7,75,7,5,10,6,6,13,3,2,11,11,3,2,5,28,15,6,18, + 18,5,6,4,3,16,1,7,18,7,36,3,5,3,1,7,1,9,1,10,7,2,4,2,6,2,9,7,4,3,32,12,3,7,10,2,23,16,3,1,12,3,31,4,11,1,3,8,9,5,1,30,15,6,12,3,2,2,11,19,9, + 14,2,6,2,3,19,13,17,5,3,3,25,3,14,1,1,1,36,1,3,2,19,3,13,36,9,13,31,6,4,16,34,2,5,4,2,3,3,5,1,1,1,4,3,1,17,3,2,3,5,3,1,3,2,3,5,6,3,12,11,1,3, + 1,2,26,7,12,7,2,14,3,3,7,7,11,25,25,28,16,4,36,1,2,1,6,2,1,9,3,27,17,4,3,4,13,4,1,3,2,2,1,10,4,2,4,6,3,8,2,1,18,1,1,24,2,2,4,33,2,3,63,7,1,6, + 40,7,3,4,4,2,4,15,18,1,16,1,1,11,2,41,14,1,3,18,13,3,2,4,16,2,17,7,15,24,7,18,13,44,2,2,3,6,1,1,7,5,1,7,1,4,3,3,5,10,8,2,3,1,8,1,1,27,4,2,1, + 12,1,2,1,10,6,1,6,7,5,2,3,7,11,5,11,3,6,6,2,3,15,4,9,1,1,2,1,2,11,2,8,12,8,5,4,2,3,1,5,2,2,1,14,1,12,11,4,1,11,17,17,4,3,2,5,5,7,3,1,5,9,9,8, + 2,5,6,6,13,13,2,1,2,6,1,2,2,49,4,9,1,2,10,16,7,8,4,3,2,23,4,58,3,29,1,14,19,19,11,11,2,7,5,1,3,4,6,2,18,5,12,12,17,17,3,3,2,4,1,6,2,3,4,3,1, + 1,1,1,5,1,1,9,1,3,1,3,6,1,8,1,1,2,6,4,14,3,1,4,11,4,1,3,32,1,2,4,13,4,1,2,4,2,1,3,1,11,1,4,2,1,4,4,6,3,5,1,6,5,7,6,3,23,3,5,3,5,3,3,13,3,9,10, + 1,12,10,2,3,18,13,7,160,52,4,2,2,3,2,14,5,4,12,4,6,4,1,20,4,11,6,2,12,27,1,4,1,2,2,7,4,5,2,28,3,7,25,8,3,19,3,6,10,2,2,1,10,2,5,4,1,3,4,1,5, + 3,2,6,9,3,6,2,16,3,3,16,4,5,5,3,2,1,2,16,15,8,2,6,21,2,4,1,22,5,8,1,1,21,11,2,1,11,11,19,13,12,4,2,3,2,3,6,1,8,11,1,4,2,9,5,2,1,11,2,9,1,1,2, + 14,31,9,3,4,21,14,4,8,1,7,2,2,2,5,1,4,20,3,3,4,10,1,11,9,8,2,1,4,5,14,12,14,2,17,9,6,31,4,14,1,20,13,26,5,2,7,3,6,13,2,4,2,19,6,2,2,18,9,3,5, + 12,12,14,4,6,2,3,6,9,5,22,4,5,25,6,4,8,5,2,6,27,2,35,2,16,3,7,8,8,6,6,5,9,17,2,20,6,19,2,13,3,1,1,1,4,17,12,2,14,7,1,4,18,12,38,33,2,10,1,1, + 2,13,14,17,11,50,6,33,20,26,74,16,23,45,50,13,38,33,6,6,7,4,4,2,1,3,2,5,8,7,8,9,3,11,21,9,13,1,3,10,6,7,1,2,2,18,5,5,1,9,9,2,68,9,19,13,2,5, + 1,4,4,7,4,13,3,9,10,21,17,3,26,2,1,5,2,4,5,4,1,7,4,7,3,4,2,1,6,1,1,20,4,1,9,2,2,1,3,3,2,3,2,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,3,2,10,3,5,3,4,4, + 3,4,16,1,6,1,10,2,4,2,1,1,2,10,11,2,2,3,1,24,31,4,10,10,2,5,12,16,164,15,4,16,7,9,15,19,17,1,2,1,1,5,1,1,1,1,1,3,1,4,3,1,3,1,3,1,2,1,1,3,3,7, + 2,8,1,2,2,2,1,3,4,3,7,8,12,92,2,10,3,1,3,14,5,25,16,42,4,7,7,4,2,21,5,27,26,27,21,25,30,31,2,1,5,13,3,22,5,6,6,11,9,12,1,5,9,7,5,5,22,60,3,5, + 13,1,1,8,1,1,3,3,2,1,9,3,3,18,4,1,2,3,7,6,3,1,2,3,9,1,3,1,3,2,1,3,1,1,1,2,1,11,3,1,6,9,1,3,2,3,1,2,1,5,1,1,4,3,4,1,2,2,4,4,1,7,2,1,2,2,3,5,13, + 18,3,4,14,9,9,4,16,3,7,5,8,2,6,48,28,3,1,1,4,2,14,8,2,9,2,1,15,2,4,3,2,10,16,12,8,7,1,1,3,1,1,1,2,7,4,1,6,4,38,39,16,23,7,15,15,3,2,12,7,21, + 37,27,6,5,4,8,2,10,8,8,6,5,1,2,1,3,24,1,16,17,9,23,10,17,6,1,51,55,44,13,294,9,3,6,2,4,2,2,15,1,1,1,13,21,17,68,14,8,9,4,1,4,9,3,11,7,1,1,1, + 5,6,3,2,1,1,1,2,3,8,1,2,2,4,1,5,5,2,1,4,3,7,13,4,1,4,1,3,1,1,1,5,5,10,1,6,1,5,2,1,5,2,4,1,4,5,7,3,18,2,9,11,32,4,3,3,2,4,7,11,16,9,11,8,13,38, + 32,8,4,2,1,1,2,1,2,4,4,1,1,1,4,1,21,3,11,1,16,1,1,6,1,3,2,4,9,8,57,7,44,1,3,3,13,3,10,1,1,7,5,2,7,21,47,63,3,15,4,7,1,16,1,1,2,8,2,3,42,15,4, + 1,29,7,22,10,3,78,16,12,20,18,4,67,11,5,1,3,15,6,21,31,32,27,18,13,71,35,5,142,4,10,1,2,50,19,33,16,35,37,16,19,27,7,1,133,19,1,4,8,7,20,1,4, + 4,1,10,3,1,6,1,2,51,5,40,15,24,43,22928,11,1,13,154,70,3,1,1,7,4,10,1,2,1,1,2,1,2,1,2,2,1,1,2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1, + 3,2,1,1,1,1,2,1,1, + }; + static ImWchar base_ranges[] = // not zero-terminated + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00)*2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; } -ImFontAtlas::~ImFontAtlas() +const ImWchar* ImFontAtlas::GetGlyphRangesCyrillic() { - Clear(); + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement + 0x2DE0, 0x2DFF, // Cyrillic Extended-A + 0xA640, 0xA69F, // Cyrillic Extended-B + 0, + }; + return &ranges[0]; } -void ImFontAtlas::ClearInputData() +const ImWchar* ImFontAtlas::GetGlyphRangesThai() { - for (int i = 0; i < ConfigData.Size; i++) - if (ConfigData[i].FontData && ConfigData[i].FontDataOwnedByAtlas) - { - ImGui::MemFree(ConfigData[i].FontData); - ConfigData[i].FontData = NULL; - } - - // When clearing this we lose access to the font name and other information used to build the font. - for (int i = 0; i < Fonts.Size; i++) - if (Fonts[i]->ConfigData >= ConfigData.Data && Fonts[i]->ConfigData < ConfigData.Data + ConfigData.Size) - { - Fonts[i]->ConfigData = NULL; - Fonts[i]->ConfigDataCount = 0; - } - ConfigData.clear(); - CustomRects.clear(); - for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) - CustomRectIds[n] = -1; + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x2010, 0x205E, // Punctuations + 0x0E00, 0x0E7F, // Thai + 0, + }; + return &ranges[0]; } -void ImFontAtlas::ClearTexData() +const ImWchar* ImFontAtlas::GetGlyphRangesVietnamese() { - if (TexPixelsAlpha8) - ImGui::MemFree(TexPixelsAlpha8); - if (TexPixelsRGBA32) - ImGui::MemFree(TexPixelsRGBA32); - TexPixelsAlpha8 = NULL; - TexPixelsRGBA32 = NULL; + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x0102, 0x0103, + 0x0110, 0x0111, + 0x0128, 0x0129, + 0x0168, 0x0169, + 0x01A0, 0x01A1, + 0x01AF, 0x01B0, + 0x1EA0, 0x1EF9, + 0, + }; + return &ranges[0]; } -void ImFontAtlas::ClearFonts() +//----------------------------------------------------------------------------- +// [SECTION] ImFontGlyphRangesBuilder +//----------------------------------------------------------------------------- + +void ImFontGlyphRangesBuilder::AddText(const char* text, const char* text_end) { - for (int i = 0; i < Fonts.Size; i++) - IM_DELETE(Fonts[i]); - Fonts.clear(); + while (text_end ? (text < text_end) : *text) + { + unsigned int c = 0; + int c_len = ImTextCharFromUtf8(&c, text, text_end); + text += c_len; + if (c_len == 0) + break; + AddChar((ImWchar)c); + } } -void ImFontAtlas::Clear() +void ImFontGlyphRangesBuilder::AddRanges(const ImWchar* ranges) { - ClearInputData(); - ClearTexData(); - ClearFonts(); + for (; ranges[0]; ranges += 2) + for (unsigned int c = ranges[0]; c <= ranges[1] && c <= IM_UNICODE_CODEPOINT_MAX; c++) //-V560 + AddChar((ImWchar)c); } -void ImFontAtlas::GetTexDataAsAlpha8(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel) +void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) { - // Build atlas on demand - if (TexPixelsAlpha8 == NULL) - { - if (ConfigData.empty()) - AddFontDefault(); - Build(); - } + const int max_codepoint = IM_UNICODE_CODEPOINT_MAX; + for (int n = 0; n <= max_codepoint; n++) + if (GetBit(n)) + { + out_ranges->push_back((ImWchar)n); + while (n < max_codepoint && GetBit(n + 1)) + n++; + out_ranges->push_back((ImWchar)n); + } + out_ranges->push_back(0); +} - *out_pixels = TexPixelsAlpha8; - if (out_width) - *out_width = TexWidth; - if (out_height) - *out_height = TexHeight; - if (out_bytes_per_pixel) - *out_bytes_per_pixel = 1; +//----------------------------------------------------------------------------- +// [SECTION] ImFont +//----------------------------------------------------------------------------- + +ImFont::ImFont() +{ + FontSize = 0.0f; + FallbackAdvanceX = 0.0f; + FallbackChar = (ImWchar)-1; + EllipsisChar = (ImWchar)-1; + EllipsisWidth = EllipsisCharStep = 0.0f; + EllipsisCharCount = 0; + FallbackGlyph = NULL; + ContainerAtlas = NULL; + ConfigData = NULL; + ConfigDataCount = 0; + DirtyLookupTables = false; + Scale = 1.0f; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; + memset(Used4kPagesMap, 0, sizeof(Used4kPagesMap)); } -void ImFontAtlas::GetTexDataAsRGBA32(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel) +ImFont::~ImFont() { - // Convert to RGBA32 format on demand - // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp - if (!TexPixelsRGBA32) - { - unsigned char *pixels = NULL; - GetTexDataAsAlpha8(&pixels, NULL, NULL); - if (pixels) - { - TexPixelsRGBA32 = (unsigned int *) ImGui::MemAlloc((size_t) (TexWidth * TexHeight * 4)); - const unsigned char *src = pixels; - unsigned int *dst = TexPixelsRGBA32; - for (int n = TexWidth * TexHeight; n > 0; n--) - *dst++ = IM_COL32(255, 255, 255, (unsigned int) (*src++)); - } - } + ClearOutputData(); +} - *out_pixels = (unsigned char *) TexPixelsRGBA32; - if (out_width) - *out_width = TexWidth; - if (out_height) - *out_height = TexHeight; - if (out_bytes_per_pixel) - *out_bytes_per_pixel = 4; +void ImFont::ClearOutputData() +{ + FontSize = 0.0f; + FallbackAdvanceX = 0.0f; + Glyphs.clear(); + IndexAdvanceX.clear(); + IndexLookup.clear(); + FallbackGlyph = NULL; + ContainerAtlas = NULL; + DirtyLookupTables = true; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; } -ImFont *ImFontAtlas::AddFont(const ImFontConfig *font_cfg) +static ImWchar FindFirstExistingGlyph(ImFont* font, const ImWchar* candidate_chars, int candidate_chars_count) { - IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); - IM_ASSERT(font_cfg->SizePixels > 0.0f); + for (int n = 0; n < candidate_chars_count; n++) + if (font->FindGlyphNoFallback(candidate_chars[n]) != NULL) + return candidate_chars[n]; + return (ImWchar)-1; +} - // Create new font - if (!font_cfg->MergeMode) - Fonts.push_back(IM_NEW(ImFont)); - else - IM_ASSERT(!Fonts.empty()); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. - - ConfigData.push_back(*font_cfg); - ImFontConfig &new_font_cfg = ConfigData.back(); - if (!new_font_cfg.DstFont) - new_font_cfg.DstFont = Fonts.back(); - if (!new_font_cfg.FontDataOwnedByAtlas) - { - new_font_cfg.FontData = ImGui::MemAlloc(new_font_cfg.FontDataSize); - new_font_cfg.FontDataOwnedByAtlas = true; - memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t) new_font_cfg.FontDataSize); - } - - // Invalidate texture - ClearTexData(); - return new_font_cfg.DstFont; +void ImFont::BuildLookupTable() +{ + int max_codepoint = 0; + for (int i = 0; i != Glyphs.Size; i++) + max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); + + // Build lookup table + IM_ASSERT(Glyphs.Size > 0 && "Font has not loaded glyph!"); + IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved + IndexAdvanceX.clear(); + IndexLookup.clear(); + DirtyLookupTables = false; + memset(Used4kPagesMap, 0, sizeof(Used4kPagesMap)); + GrowIndex(max_codepoint + 1); + for (int i = 0; i < Glyphs.Size; i++) + { + int codepoint = (int)Glyphs[i].Codepoint; + IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; + IndexLookup[codepoint] = (ImWchar)i; + + // Mark 4K page as used + const int page_n = codepoint / 4096; + Used4kPagesMap[page_n >> 3] |= 1 << (page_n & 7); + } + + // Create a glyph to handle TAB + // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) + if (FindGlyph((ImWchar)' ')) + { + if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times (FIXME: Flaky) + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& tab_glyph = Glyphs.back(); + tab_glyph = *FindGlyph((ImWchar)' '); + tab_glyph.Codepoint = '\t'; + tab_glyph.AdvanceX *= IM_TABSIZE; + IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX; + IndexLookup[(int)tab_glyph.Codepoint] = (ImWchar)(Glyphs.Size - 1); + } + + // Mark special glyphs as not visible (note that AddGlyph already mark as non-visible glyphs with zero-size polygons) + SetGlyphVisible((ImWchar)' ', false); + SetGlyphVisible((ImWchar)'\t', false); + + // Setup Fallback character + const ImWchar fallback_chars[] = { (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' }; + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackChar = FindFirstExistingGlyph(this, fallback_chars, IM_ARRAYSIZE(fallback_chars)); + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackGlyph = &Glyphs.back(); + FallbackChar = (ImWchar)FallbackGlyph->Codepoint; + } + } + FallbackAdvanceX = FallbackGlyph->AdvanceX; + for (int i = 0; i < max_codepoint + 1; i++) + if (IndexAdvanceX[i] < 0.0f) + IndexAdvanceX[i] = FallbackAdvanceX; + + // Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). + // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. + // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots. + const ImWchar ellipsis_chars[] = { (ImWchar)0x2026, (ImWchar)0x0085 }; + const ImWchar dots_chars[] = { (ImWchar)'.', (ImWchar)0xFF0E }; + if (EllipsisChar == (ImWchar)-1) + EllipsisChar = FindFirstExistingGlyph(this, ellipsis_chars, IM_ARRAYSIZE(ellipsis_chars)); + const ImWchar dot_char = FindFirstExistingGlyph(this, dots_chars, IM_ARRAYSIZE(dots_chars)); + if (EllipsisChar != (ImWchar)-1) + { + EllipsisCharCount = 1; + EllipsisWidth = EllipsisCharStep = FindGlyph(EllipsisChar)->X1; + } + else if (dot_char != (ImWchar)-1) + { + const ImFontGlyph* glyph = FindGlyph(dot_char); + EllipsisChar = dot_char; + EllipsisCharCount = 3; + EllipsisCharStep = (glyph->X1 - glyph->X0) + 1.0f; + EllipsisWidth = EllipsisCharStep * 3.0f - 1.0f; + } } -// Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) -static unsigned int stb_decompress_length(unsigned char *input); -static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsigned int length); -static const char *GetDefaultCompressedFontDataTTFBase85(); -static unsigned int Decode85Byte(char c) +// API is designed this way to avoid exposing the 4K page size +// e.g. use with IsGlyphRangeUnused(0, 255) +bool ImFont::IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last) { - return c >= '\\' ? c - 36 : c - 35; + unsigned int page_begin = (c_begin / 4096); + unsigned int page_last = (c_last / 4096); + for (unsigned int page_n = page_begin; page_n <= page_last; page_n++) + if ((page_n >> 3) < sizeof(Used4kPagesMap)) + if (Used4kPagesMap[page_n >> 3] & (1 << (page_n & 7))) + return false; + return true; } -static void Decode85(const unsigned char *src, unsigned char *dst) + +void ImFont::SetGlyphVisible(ImWchar c, bool visible) { - while (*src) - { - unsigned int tmp = Decode85Byte(src[0]) + 85 * (Decode85Byte(src[1]) + 85 * (Decode85Byte(src[2]) + 85 * (Decode85Byte(src[3]) + 85 * Decode85Byte(src[4])))); - dst[0] = ((tmp >> 0) & 0xFF); - dst[1] = ((tmp >> 8) & 0xFF); - dst[2] = ((tmp >> 16) & 0xFF); - dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. - src += 5; - dst += 4; - } + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)c)) + glyph->Visible = visible ? 1 : 0; } -// Load embedded ProggyClean.ttf at size 13, disable oversampling -ImFont *ImFontAtlas::AddFontDefault(const ImFontConfig *font_cfg_template) -{ - ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); - if (!font_cfg_template) - { - font_cfg.OversampleH = font_cfg.OversampleV = 1; - font_cfg.PixelSnapH = true; - } - if (font_cfg.Name[0] == '\0') - strcpy(font_cfg.Name, "ProggyClean.ttf, 13px"); - if (font_cfg.SizePixels <= 0.0f) - font_cfg.SizePixels = 13.0f; - - const char *ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); - ImFont *font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, GetGlyphRangesDefault()); - return font; -} - -ImFont *ImFontAtlas::AddFontFromFileTTF(const char *filename, float size_pixels, const ImFontConfig *font_cfg_template, const ImWchar *glyph_ranges) -{ - int data_size = 0; - void *data = ImFileLoadToMemory(filename, "rb", &data_size, 0); - if (!data) - { - IM_ASSERT(0); // Could not load file. - return NULL; - } - ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); - if (font_cfg.Name[0] == '\0') - { - // Store a short copy of filename into into the font name for convenience - const char *p; - for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) - { - } - snprintf(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); - } - return AddFontFromMemoryTTF(data, data_size, size_pixels, &font_cfg, glyph_ranges); +void ImFont::GrowIndex(int new_size) +{ + IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size); + if (new_size <= IndexLookup.Size) + return; + IndexAdvanceX.resize(new_size, -1.0f); + IndexLookup.resize(new_size, (ImWchar)-1); } -// NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). -ImFont *ImFontAtlas::AddFontFromMemoryTTF(void *ttf_data, int ttf_size, float size_pixels, const ImFontConfig *font_cfg_template, const ImWchar *glyph_ranges) +// x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero. +// Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis). +// 'cfg' is not necessarily == 'this->ConfigData' because multiple source fonts+configs can be used to build one target font. +void ImFont::AddGlyph(const ImFontConfig* cfg, ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) { - ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); - IM_ASSERT(font_cfg.FontData == NULL); - font_cfg.FontData = ttf_data; - font_cfg.FontDataSize = ttf_size; - font_cfg.SizePixels = size_pixels; - if (glyph_ranges) - font_cfg.GlyphRanges = glyph_ranges; - return AddFont(&font_cfg); + if (cfg != NULL) + { + // Clamp & recenter if needed + const float advance_x_original = advance_x; + advance_x = ImClamp(advance_x, cfg->GlyphMinAdvanceX, cfg->GlyphMaxAdvanceX); + if (advance_x != advance_x_original) + { + float char_off_x = cfg->PixelSnapH ? ImTrunc((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; + x0 += char_off_x; + x1 += char_off_x; + } + + // Snap to pixel + if (cfg->PixelSnapH) + advance_x = IM_ROUND(advance_x); + + // Bake spacing + advance_x += cfg->GlyphExtraSpacing.x; + } + + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& glyph = Glyphs.back(); + glyph.Codepoint = (unsigned int)codepoint; + glyph.Visible = (x0 != x1) && (y0 != y1); + glyph.Colored = false; + glyph.X0 = x0; + glyph.Y0 = y0; + glyph.X1 = x1; + glyph.Y1 = y1; + glyph.U0 = u0; + glyph.V0 = v0; + glyph.U1 = u1; + glyph.V1 = v1; + glyph.AdvanceX = advance_x; + + // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) + // We use (U1-U0)*TexWidth instead of X1-X0 to account for oversampling. + float pad = ContainerAtlas->TexGlyphPadding + 0.99f; + DirtyLookupTables = true; + MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + pad) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + pad); } -ImFont *ImFontAtlas::AddFontFromMemoryCompressedTTF(const void *compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig *font_cfg_template, const ImWchar *glyph_ranges) +void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) { - const unsigned int buf_decompressed_size = stb_decompress_length((unsigned char *) compressed_ttf_data); - unsigned char *buf_decompressed_data = (unsigned char *) ImGui::MemAlloc(buf_decompressed_size); - stb_decompress(buf_decompressed_data, (unsigned char *) compressed_ttf_data, (unsigned int) compressed_ttf_size); + IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. + unsigned int index_size = (unsigned int)IndexLookup.Size; - ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); - IM_ASSERT(font_cfg.FontData == NULL); - font_cfg.FontDataOwnedByAtlas = true; - return AddFontFromMemoryTTF(buf_decompressed_data, (int) buf_decompressed_size, size_pixels, &font_cfg, glyph_ranges); + if (dst < index_size && IndexLookup.Data[dst] == (ImWchar)-1 && !overwrite_dst) // 'dst' already exists + return; + if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op + return; + + GrowIndex(dst + 1); + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImWchar)-1; + IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; } -ImFont *ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char *compressed_ttf_data_base85, float size_pixels, const ImFontConfig *font_cfg, const ImWchar *glyph_ranges) +const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const { - int compressed_ttf_size = (((int) strlen(compressed_ttf_data_base85) + 4) / 5) * 4; - void *compressed_ttf = ImGui::MemAlloc((size_t) compressed_ttf_size); - Decode85((const unsigned char *) compressed_ttf_data_base85, (unsigned char *) compressed_ttf); - ImFont *font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); - ImGui::MemFree(compressed_ttf); - return font; + if (c >= (size_t)IndexLookup.Size) + return FallbackGlyph; + const ImWchar i = IndexLookup.Data[c]; + if (i == (ImWchar)-1) + return FallbackGlyph; + return &Glyphs.Data[i]; } -int ImFontAtlas::AddCustomRectRegular(unsigned int id, int width, int height) +const ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) const { - IM_ASSERT(id >= 0x10000); - IM_ASSERT(width > 0 && width <= 0xFFFF); - IM_ASSERT(height > 0 && height <= 0xFFFF); - CustomRect r; - r.ID = id; - r.Width = (unsigned short) width; - r.Height = (unsigned short) height; - CustomRects.push_back(r); - return CustomRects.Size - 1; // Return index + if (c >= (size_t)IndexLookup.Size) + return NULL; + const ImWchar i = IndexLookup.Data[c]; + if (i == (ImWchar)-1) + return NULL; + return &Glyphs.Data[i]; } -int ImFontAtlas::AddCustomRectFontGlyph(ImFont *font, ImWchar id, int width, int height, float advance_x, const ImVec2 &offset) +// Wrapping skips upcoming blanks +static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end) { - IM_ASSERT(font != NULL); - IM_ASSERT(width > 0 && width <= 0xFFFF); - IM_ASSERT(height > 0 && height <= 0xFFFF); - CustomRect r; - r.ID = id; - r.Width = (unsigned short) width; - r.Height = (unsigned short) height; - r.GlyphAdvanceX = advance_x; - r.GlyphOffset = offset; - r.Font = font; - CustomRects.push_back(r); - return CustomRects.Size - 1; // Return index + while (text < text_end && ImCharIsBlankA(*text)) + text++; + if (*text == '\n') + text++; + return text; } -void ImFontAtlas::CalcCustomRectUV(const CustomRect *rect, ImVec2 *out_uv_min, ImVec2 *out_uv_max) -{ - IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates - IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed - *out_uv_min = ImVec2((float) rect->X * TexUvScale.x, (float) rect->Y * TexUvScale.y); - *out_uv_max = ImVec2((float) (rect->X + rect->Width) * TexUvScale.x, (float) (rect->Y + rect->Height) * TexUvScale.y); -} - -bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2 *out_offset, ImVec2 *out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) -{ - if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_Count_) - return false; - if (Flags & ImFontAtlasFlags_NoMouseCursors) - return false; - - ImFontAtlas::CustomRect &r = CustomRects[CustomRectIds[0]]; - IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); - ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float) r.X, (float) r.Y); - ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; - *out_size = size; - *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; - out_uv_border[0] = (pos) *TexUvScale; - out_uv_border[1] = (pos + size) * TexUvScale; - pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; - out_uv_fill[0] = (pos) *TexUvScale; - out_uv_fill[1] = (pos + size) * TexUvScale; - return true; -} - -bool ImFontAtlas::Build() -{ - return ImFontAtlasBuildWithStbTruetype(this); -} - -void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) -{ - for (unsigned int i = 0; i < 256; i++) - { - unsigned int value = (unsigned int) (i * in_brighten_factor); - out_table[i] = value > 255 ? 255 : (value & 0xFF); - } -} - -void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char *pixels, int x, int y, int w, int h, int stride) -{ - unsigned char *data = pixels + x + y * stride; - for (int j = h; j > 0; j--, data += stride) - for (int i = 0; i < w; i++) - data[i] = table[data[i]]; -} - -bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas *atlas) -{ - IM_ASSERT(atlas->ConfigData.Size > 0); - - ImFontAtlasBuildRegisterDefaultCustomRects(atlas); - - atlas->TexID = NULL; - atlas->TexWidth = atlas->TexHeight = 0; - atlas->TexUvScale = ImVec2(0.0f, 0.0f); - atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); - atlas->ClearTexData(); - - // Count glyphs/ranges - int total_glyphs_count = 0; - int total_ranges_count = 0; - for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) - { - ImFontConfig &cfg = atlas->ConfigData[input_i]; - if (!cfg.GlyphRanges) - cfg.GlyphRanges = atlas->GetGlyphRangesDefault(); - for (const ImWchar *in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, total_ranges_count++) - total_glyphs_count += (in_range[1] - in_range[0]) + 1; - } - - // We need a width for the skyline algorithm. Using a dumb heuristic here to decide of width. User can override TexDesiredWidth and TexGlyphPadding if they wish. - // Width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. - atlas->TexWidth = (atlas->TexDesiredWidth > 0) ? atlas->TexDesiredWidth : (total_glyphs_count > 4000) ? 4096 : - (total_glyphs_count > 2000) ? 2048 : - (total_glyphs_count > 1000) ? 1024 : - 512; - atlas->TexHeight = 0; - - // Start packing - const int max_tex_height = 1024 * 32; - stbtt_pack_context spc = {}; - if (!stbtt_PackBegin(&spc, NULL, atlas->TexWidth, max_tex_height, 0, atlas->TexGlyphPadding, NULL)) - return false; - stbtt_PackSetOversampling(&spc, 1, 1); - - // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). - ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); - - // Initialize font information (so we can error without any cleanup) - struct ImFontTempBuildData - { - stbtt_fontinfo FontInfo; - stbrp_rect *Rects; - int RectsCount; - stbtt_pack_range *Ranges; - int RangesCount; - }; - ImFontTempBuildData *tmp_array = (ImFontTempBuildData *) ImGui::MemAlloc((size_t) atlas->ConfigData.Size * sizeof(ImFontTempBuildData)); - for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) - { - ImFontConfig &cfg = atlas->ConfigData[input_i]; - ImFontTempBuildData &tmp = tmp_array[input_i]; - IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); - - const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char *) cfg.FontData, cfg.FontNo); - IM_ASSERT(font_offset >= 0); - if (!stbtt_InitFont(&tmp.FontInfo, (unsigned char *) cfg.FontData, font_offset)) - { - atlas->TexWidth = atlas->TexHeight = 0; // Reset output on failure - ImGui::MemFree(tmp_array); - return false; - } - } - - // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) - int buf_packedchars_n = 0, buf_rects_n = 0, buf_ranges_n = 0; - stbtt_packedchar *buf_packedchars = (stbtt_packedchar *) ImGui::MemAlloc(total_glyphs_count * sizeof(stbtt_packedchar)); - stbrp_rect *buf_rects = (stbrp_rect *) ImGui::MemAlloc(total_glyphs_count * sizeof(stbrp_rect)); - stbtt_pack_range *buf_ranges = (stbtt_pack_range *) ImGui::MemAlloc(total_ranges_count * sizeof(stbtt_pack_range)); - memset(buf_packedchars, 0, total_glyphs_count * sizeof(stbtt_packedchar)); - memset(buf_rects, 0, total_glyphs_count * sizeof(stbrp_rect)); // Unnecessary but let's clear this for the sake of sanity. - memset(buf_ranges, 0, total_ranges_count * sizeof(stbtt_pack_range)); - - // First font pass: pack all glyphs (no rendering at this point, we are working with rectangles in an infinitely tall texture at this point) - for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) - { - ImFontConfig &cfg = atlas->ConfigData[input_i]; - ImFontTempBuildData &tmp = tmp_array[input_i]; - - // Setup ranges - int font_glyphs_count = 0; - int font_ranges_count = 0; - for (const ImWchar *in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, font_ranges_count++) - font_glyphs_count += (in_range[1] - in_range[0]) + 1; - tmp.Ranges = buf_ranges + buf_ranges_n; - tmp.RangesCount = font_ranges_count; - buf_ranges_n += font_ranges_count; - for (int i = 0; i < font_ranges_count; i++) - { - const ImWchar *in_range = &cfg.GlyphRanges[i * 2]; - stbtt_pack_range &range = tmp.Ranges[i]; - range.font_size = cfg.SizePixels; - range.first_unicode_codepoint_in_range = in_range[0]; - range.num_chars = (in_range[1] - in_range[0]) + 1; - range.chardata_for_range = buf_packedchars + buf_packedchars_n; - buf_packedchars_n += range.num_chars; - } - - // Pack - tmp.Rects = buf_rects + buf_rects_n; - tmp.RectsCount = font_glyphs_count; - buf_rects_n += font_glyphs_count; - stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); - int n = stbtt_PackFontRangesGatherRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); - IM_ASSERT(n == font_glyphs_count); - stbrp_pack_rects((stbrp_context *) spc.pack_info, tmp.Rects, n); - - // Extend texture height - for (int i = 0; i < n; i++) - if (tmp.Rects[i].was_packed) - atlas->TexHeight = ImMax(atlas->TexHeight, tmp.Rects[i].y + tmp.Rects[i].h); - } - IM_ASSERT(buf_rects_n == total_glyphs_count); - IM_ASSERT(buf_packedchars_n == total_glyphs_count); - IM_ASSERT(buf_ranges_n == total_ranges_count); - - // Create texture - atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); - atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); - atlas->TexPixelsAlpha8 = (unsigned char *) ImGui::MemAlloc(atlas->TexWidth * atlas->TexHeight); - memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); - spc.pixels = atlas->TexPixelsAlpha8; - spc.height = atlas->TexHeight; - - // Second pass: render font characters - for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) - { - ImFontConfig &cfg = atlas->ConfigData[input_i]; - ImFontTempBuildData &tmp = tmp_array[input_i]; - stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); - stbtt_PackFontRangesRenderIntoRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); - if (cfg.RasterizerMultiply != 1.0f) - { - unsigned char multiply_table[256]; - ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); - for (const stbrp_rect *r = tmp.Rects; r != tmp.Rects + tmp.RectsCount; r++) - if (r->was_packed) - ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, spc.pixels, r->x, r->y, r->w, r->h, spc.stride_in_bytes); - } - tmp.Rects = NULL; - } - - // End packing - stbtt_PackEnd(&spc); - ImGui::MemFree(buf_rects); - buf_rects = NULL; - - // Third pass: setup ImFont and glyphs for runtime - for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) - { - ImFontConfig &cfg = atlas->ConfigData[input_i]; - ImFontTempBuildData &tmp = tmp_array[input_i]; - ImFont *dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true) - - const float font_scale = stbtt_ScaleForPixelHeight(&tmp.FontInfo, cfg.SizePixels); - int unscaled_ascent, unscaled_descent, unscaled_line_gap; - stbtt_GetFontVMetrics(&tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); - - const float ascent = unscaled_ascent * font_scale; - const float descent = unscaled_descent * font_scale; - ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); - const float off_x = cfg.GlyphOffset.x; - const float off_y = cfg.GlyphOffset.y + (float) (int) (dst_font->Ascent + 0.5f); - - for (int i = 0; i < tmp.RangesCount; i++) - { - stbtt_pack_range &range = tmp.Ranges[i]; - for (int char_idx = 0; char_idx < range.num_chars; char_idx += 1) - { - const stbtt_packedchar &pc = range.chardata_for_range[char_idx]; - if (!pc.x0 && !pc.x1 && !pc.y0 && !pc.y1) - continue; - - const int codepoint = range.first_unicode_codepoint_in_range + char_idx; - if (cfg.MergeMode && dst_font->FindGlyph((unsigned short) codepoint)) - continue; - - stbtt_aligned_quad q; - float dummy_x = 0.0f, dummy_y = 0.0f; - stbtt_GetPackedQuad(range.chardata_for_range, atlas->TexWidth, atlas->TexHeight, char_idx, &dummy_x, &dummy_y, &q, 0); - dst_font->AddGlyph((ImWchar) codepoint, q.x0 + off_x, q.y0 + off_y, q.x1 + off_x, q.y1 + off_y, q.s0, q.t0, q.s1, q.t1, pc.xadvance); - } - } - } - - // Cleanup temporaries - ImGui::MemFree(buf_packedchars); - ImGui::MemFree(buf_ranges); - ImGui::MemFree(tmp_array); - - ImFontAtlasBuildFinish(atlas); - - return true; -} - -void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas *atlas) -{ - if (atlas->CustomRectIds[0] >= 0) - return; - if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) - atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H); - else - atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, 2, 2); -} - -void ImFontAtlasBuildSetupFont(ImFontAtlas *atlas, ImFont *font, ImFontConfig *font_config, float ascent, float descent) -{ - if (!font_config->MergeMode) - { - font->ClearOutputData(); - font->FontSize = font_config->SizePixels; - font->ConfigData = font_config; - font->ContainerAtlas = atlas; - font->Ascent = ascent; - font->Descent = descent; - } - font->ConfigDataCount++; -} - -void ImFontAtlasBuildPackCustomRects(ImFontAtlas *atlas, void *pack_context_opaque) -{ - stbrp_context *pack_context = (stbrp_context *) pack_context_opaque; - - ImVector &user_rects = atlas->CustomRects; - IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. - - ImVector pack_rects; - pack_rects.resize(user_rects.Size); - memset(pack_rects.Data, 0, sizeof(stbrp_rect) * user_rects.Size); - for (int i = 0; i < user_rects.Size; i++) - { - pack_rects[i].w = user_rects[i].Width; - pack_rects[i].h = user_rects[i].Height; - } - stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); - for (int i = 0; i < pack_rects.Size; i++) - if (pack_rects[i].was_packed) - { - user_rects[i].X = pack_rects[i].x; - user_rects[i].Y = pack_rects[i].y; - IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); - atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); - } -} - -static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas *atlas) -{ - IM_ASSERT(atlas->CustomRectIds[0] >= 0); - IM_ASSERT(atlas->TexPixelsAlpha8 != NULL); - ImFontAtlas::CustomRect &r = atlas->CustomRects[atlas->CustomRectIds[0]]; - IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); - IM_ASSERT(r.IsPacked()); - - const int w = atlas->TexWidth; - if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) - { - // Render/copy pixels - IM_ASSERT(r.Width == FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * 2 + 1 && r.Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); - for (int y = 0, n = 0; y < FONT_ATLAS_DEFAULT_TEX_DATA_H; y++) - for (int x = 0; x < FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF; x++, n++) - { - const int offset0 = (int) (r.X + x) + (int) (r.Y + y) * w; - const int offset1 = offset0 + FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; - atlas->TexPixelsAlpha8[offset0] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == '.' ? 0xFF : 0x00; - atlas->TexPixelsAlpha8[offset1] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == 'X' ? 0xFF : 0x00; - } - } - else - { - IM_ASSERT(r.Width == 2 && r.Height == 2); - const int offset = (int) (r.X) + (int) (r.Y) * w; - atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; - } - atlas->TexUvWhitePixel = ImVec2((r.X + 0.5f) * atlas->TexUvScale.x, (r.Y + 0.5f) * atlas->TexUvScale.y); -} - -void ImFontAtlasBuildFinish(ImFontAtlas *atlas) -{ - // Render into our custom data block - ImFontAtlasBuildRenderDefaultTexData(atlas); - - // Register custom rectangle glyphs - for (int i = 0; i < atlas->CustomRects.Size; i++) - { - const ImFontAtlas::CustomRect &r = atlas->CustomRects[i]; - if (r.Font == NULL || r.ID > 0x10000) - continue; - - IM_ASSERT(r.Font->ContainerAtlas == atlas); - ImVec2 uv0, uv1; - atlas->CalcCustomRectUV(&r, &uv0, &uv1); - r.Font->AddGlyph((ImWchar) r.ID, r.GlyphOffset.x, r.GlyphOffset.y, r.GlyphOffset.x + r.Width, r.GlyphOffset.y + r.Height, uv0.x, uv0.y, uv1.x, uv1.y, r.GlyphAdvanceX); - } - - // Build all fonts lookup tables - for (int i = 0; i < atlas->Fonts.Size; i++) - atlas->Fonts[i]->BuildLookupTable(); -} +// Simple word-wrapping for English, not full-featured. Please submit failing cases! +// This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. +// FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const +{ + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ -// Retrieve list of range (2 int per range, values are inclusive) -const ImWchar *ImFontAtlas::GetGlyphRangesDefault() -{ - static const ImWchar ranges[] = - { - 0x0020, - 0x00FF, // Basic Latin + Latin Supplement - 0, - }; - return &ranges[0]; -} - -const ImWchar *ImFontAtlas::GetGlyphRangesKorean() -{ - static const ImWchar ranges[] = - { - 0x0020, - 0x00FF, // Basic Latin + Latin Supplement - 0x3131, - 0x3163, // Korean alphabets - 0xAC00, - 0xD79D, // Korean characters - 0, - }; - return &ranges[0]; -} - -const ImWchar *ImFontAtlas::GetGlyphRangesChinese() -{ - static const ImWchar ranges[] = - { - 0x0020, - 0x00FF, // Basic Latin + Latin Supplement - 0x3000, - 0x30FF, // Punctuations, Hiragana, Katakana - 0x31F0, - 0x31FF, // Katakana Phonetic Extensions - 0xFF00, - 0xFFEF, // Half-width characters - 0x4e00, - 0x9FAF, // CJK Ideograms - 0, - }; - return &ranges[0]; -} - -const ImWchar *ImFontAtlas::GetGlyphRangesJapanese() -{ - // Store the 1946 ideograms code points as successive offsets from the initial unicode codepoint 0x4E00. Each offset has an implicit +1. - // This encoding is designed to helps us reduce the source code size. - // FIXME: Source a list of the revised 2136 joyo kanji list from 2010 and rebuild this. - // The current list was sourced from http://theinstructionlimit.com/author/renaudbedardrenaudbedard/page/3 - // Note that you may use ImFontAtlas::GlyphRangesBuilder to create your own ranges, by merging existing ranges or adding new characters. - static const short offsets_from_0x4E00[] = - { - -1, - 0, - 1, - 3, - 0, - 0, - 0, - 0, - 1, - 0, - 5, - 1, - 1, - 0, - 7, - 4, - 6, - 10, - 0, - 1, - 9, - 9, - 7, - 1, - 3, - 19, - 1, - 10, - 7, - 1, - 0, - 1, - 0, - 5, - 1, - 0, - 6, - 4, - 2, - 6, - 0, - 0, - 12, - 6, - 8, - 0, - 3, - 5, - 0, - 1, - 0, - 9, - 0, - 0, - 8, - 1, - 1, - 3, - 4, - 5, - 13, - 0, - 0, - 8, - 2, - 17, - 4, - 3, - 1, - 1, - 9, - 6, - 0, - 0, - 0, - 2, - 1, - 3, - 2, - 22, - 1, - 9, - 11, - 1, - 13, - 1, - 3, - 12, - 0, - 5, - 9, - 2, - 0, - 6, - 12, - 5, - 3, - 12, - 4, - 1, - 2, - 16, - 1, - 1, - 4, - 6, - 5, - 3, - 0, - 6, - 13, - 15, - 5, - 12, - 8, - 14, - 0, - 0, - 6, - 15, - 3, - 6, - 0, - 18, - 8, - 1, - 6, - 14, - 1, - 5, - 4, - 12, - 24, - 3, - 13, - 12, - 10, - 24, - 0, - 0, - 0, - 1, - 0, - 1, - 1, - 2, - 9, - 10, - 2, - 2, - 0, - 0, - 3, - 3, - 1, - 0, - 3, - 8, - 0, - 3, - 2, - 4, - 4, - 1, - 6, - 11, - 10, - 14, - 6, - 15, - 3, - 4, - 15, - 1, - 0, - 0, - 5, - 2, - 2, - 0, - 0, - 1, - 6, - 5, - 5, - 6, - 0, - 3, - 6, - 5, - 0, - 0, - 1, - 0, - 11, - 2, - 2, - 8, - 4, - 7, - 0, - 10, - 0, - 1, - 2, - 17, - 19, - 3, - 0, - 2, - 5, - 0, - 6, - 2, - 4, - 4, - 6, - 1, - 1, - 11, - 2, - 0, - 3, - 1, - 2, - 1, - 2, - 10, - 7, - 6, - 3, - 16, - 0, - 8, - 24, - 0, - 0, - 3, - 1, - 1, - 3, - 0, - 1, - 6, - 0, - 0, - 0, - 2, - 0, - 1, - 5, - 15, - 0, - 1, - 0, - 0, - 2, - 11, - 19, - 1, - 4, - 19, - 7, - 6, - 5, - 1, - 0, - 0, - 0, - 0, - 5, - 1, - 0, - 1, - 9, - 0, - 0, - 5, - 0, - 2, - 0, - 1, - 0, - 3, - 0, - 11, - 3, - 0, - 2, - 0, - 0, - 0, - 0, - 0, - 9, - 3, - 6, - 4, - 12, - 0, - 14, - 0, - 0, - 29, - 10, - 8, - 0, - 14, - 37, - 13, - 0, - 31, - 16, - 19, - 0, - 8, - 30, - 1, - 20, - 8, - 3, - 48, - 21, - 1, - 0, - 12, - 0, - 10, - 44, - 34, - 42, - 54, - 11, - 18, - 82, - 0, - 2, - 1, - 2, - 12, - 1, - 0, - 6, - 2, - 17, - 2, - 12, - 7, - 0, - 7, - 17, - 4, - 2, - 6, - 24, - 23, - 8, - 23, - 39, - 2, - 16, - 23, - 1, - 0, - 5, - 1, - 2, - 15, - 14, - 5, - 6, - 2, - 11, - 0, - 8, - 6, - 2, - 2, - 2, - 14, - 20, - 4, - 15, - 3, - 4, - 11, - 10, - 10, - 2, - 5, - 2, - 1, - 30, - 2, - 1, - 0, - 0, - 22, - 5, - 5, - 0, - 3, - 1, - 5, - 4, - 1, - 0, - 0, - 2, - 2, - 21, - 1, - 5, - 1, - 2, - 16, - 2, - 1, - 3, - 4, - 0, - 8, - 4, - 0, - 0, - 5, - 14, - 11, - 2, - 16, - 1, - 13, - 1, - 7, - 0, - 22, - 15, - 3, - 1, - 22, - 7, - 14, - 22, - 19, - 11, - 24, - 18, - 46, - 10, - 20, - 64, - 45, - 3, - 2, - 0, - 4, - 5, - 0, - 1, - 4, - 25, - 1, - 0, - 0, - 2, - 10, - 0, - 0, - 0, - 1, - 0, - 1, - 2, - 0, - 0, - 9, - 1, - 2, - 0, - 0, - 0, - 2, - 5, - 2, - 1, - 1, - 5, - 5, - 8, - 1, - 1, - 1, - 5, - 1, - 4, - 9, - 1, - 3, - 0, - 1, - 0, - 1, - 1, - 2, - 0, - 0, - 2, - 0, - 1, - 8, - 22, - 8, - 1, - 0, - 0, - 0, - 0, - 4, - 2, - 1, - 0, - 9, - 8, - 5, - 0, - 9, - 1, - 30, - 24, - 2, - 6, - 4, - 39, - 0, - 14, - 5, - 16, - 6, - 26, - 179, - 0, - 2, - 1, - 1, - 0, - 0, - 0, - 5, - 2, - 9, - 6, - 0, - 2, - 5, - 16, - 7, - 5, - 1, - 1, - 0, - 2, - 4, - 4, - 7, - 15, - 13, - 14, - 0, - 0, - 3, - 0, - 1, - 0, - 0, - 0, - 2, - 1, - 6, - 4, - 5, - 1, - 4, - 9, - 0, - 3, - 1, - 8, - 0, - 0, - 10, - 5, - 0, - 43, - 0, - 2, - 6, - 8, - 4, - 0, - 2, - 0, - 0, - 9, - 6, - 0, - 9, - 3, - 1, - 6, - 20, - 14, - 6, - 1, - 4, - 0, - 7, - 2, - 3, - 0, - 2, - 0, - 5, - 0, - 3, - 1, - 0, - 3, - 9, - 7, - 0, - 3, - 4, - 0, - 4, - 9, - 1, - 6, - 0, - 9, - 0, - 0, - 2, - 3, - 10, - 9, - 28, - 3, - 6, - 2, - 4, - 1, - 2, - 32, - 4, - 1, - 18, - 2, - 0, - 3, - 1, - 5, - 30, - 10, - 0, - 2, - 2, - 2, - 0, - 7, - 9, - 8, - 11, - 10, - 11, - 7, - 2, - 13, - 7, - 5, - 10, - 0, - 3, - 40, - 2, - 0, - 1, - 6, - 12, - 0, - 4, - 5, - 1, - 5, - 11, - 11, - 21, - 4, - 8, - 3, - 7, - 8, - 8, - 33, - 5, - 23, - 0, - 0, - 19, - 8, - 8, - 2, - 3, - 0, - 6, - 1, - 1, - 1, - 5, - 1, - 27, - 4, - 2, - 5, - 0, - 3, - 5, - 6, - 3, - 1, - 0, - 3, - 1, - 12, - 5, - 3, - 3, - 2, - 0, - 7, - 7, - 2, - 1, - 0, - 4, - 0, - 1, - 1, - 2, - 0, - 10, - 10, - 6, - 2, - 5, - 9, - 7, - 5, - 15, - 15, - 21, - 6, - 11, - 5, - 20, - 4, - 3, - 5, - 5, - 2, - 5, - 0, - 2, - 1, - 0, - 1, - 7, - 28, - 0, - 9, - 0, - 5, - 12, - 5, - 5, - 18, - 30, - 0, - 12, - 3, - 3, - 21, - 16, - 25, - 32, - 9, - 3, - 14, - 11, - 24, - 5, - 66, - 9, - 1, - 2, - 0, - 5, - 9, - 1, - 5, - 1, - 8, - 0, - 8, - 3, - 3, - 0, - 1, - 15, - 1, - 4, - 8, - 1, - 2, - 7, - 0, - 7, - 2, - 8, - 3, - 7, - 5, - 3, - 7, - 10, - 2, - 1, - 0, - 0, - 2, - 25, - 0, - 6, - 4, - 0, - 10, - 0, - 4, - 2, - 4, - 1, - 12, - 5, - 38, - 4, - 0, - 4, - 1, - 10, - 5, - 9, - 4, - 0, - 14, - 4, - 2, - 5, - 18, - 20, - 21, - 1, - 3, - 0, - 5, - 0, - 7, - 0, - 3, - 7, - 1, - 3, - 1, - 1, - 8, - 1, - 0, - 0, - 0, - 3, - 2, - 5, - 2, - 11, - 6, - 0, - 13, - 1, - 3, - 9, - 1, - 12, - 0, - 16, - 6, - 2, - 1, - 0, - 2, - 1, - 12, - 6, - 13, - 11, - 2, - 0, - 28, - 1, - 7, - 8, - 14, - 13, - 8, - 13, - 0, - 2, - 0, - 5, - 4, - 8, - 10, - 2, - 37, - 42, - 19, - 6, - 6, - 7, - 4, - 14, - 11, - 18, - 14, - 80, - 7, - 6, - 0, - 4, - 72, - 12, - 36, - 27, - 7, - 7, - 0, - 14, - 17, - 19, - 164, - 27, - 0, - 5, - 10, - 7, - 3, - 13, - 6, - 14, - 0, - 2, - 2, - 5, - 3, - 0, - 6, - 13, - 0, - 0, - 10, - 29, - 0, - 4, - 0, - 3, - 13, - 0, - 3, - 1, - 6, - 51, - 1, - 5, - 28, - 2, - 0, - 8, - 0, - 20, - 2, - 4, - 0, - 25, - 2, - 10, - 13, - 10, - 0, - 16, - 4, - 0, - 1, - 0, - 2, - 1, - 7, - 0, - 1, - 8, - 11, - 0, - 0, - 1, - 2, - 7, - 2, - 23, - 11, - 6, - 6, - 4, - 16, - 2, - 2, - 2, - 0, - 22, - 9, - 3, - 3, - 5, - 2, - 0, - 15, - 16, - 21, - 2, - 9, - 20, - 15, - 15, - 5, - 3, - 9, - 1, - 0, - 0, - 1, - 7, - 7, - 5, - 4, - 2, - 2, - 2, - 38, - 24, - 14, - 0, - 0, - 15, - 5, - 6, - 24, - 14, - 5, - 5, - 11, - 0, - 21, - 12, - 0, - 3, - 8, - 4, - 11, - 1, - 8, - 0, - 11, - 27, - 7, - 2, - 4, - 9, - 21, - 59, - 0, - 1, - 39, - 3, - 60, - 62, - 3, - 0, - 12, - 11, - 0, - 3, - 30, - 11, - 0, - 13, - 88, - 4, - 15, - 5, - 28, - 13, - 1, - 4, - 48, - 17, - 17, - 4, - 28, - 32, - 46, - 0, - 16, - 0, - 18, - 11, - 1, - 8, - 6, - 38, - 11, - 2, - 6, - 11, - 38, - 2, - 0, - 45, - 3, - 11, - 2, - 7, - 8, - 4, - 30, - 14, - 17, - 2, - 1, - 1, - 65, - 18, - 12, - 16, - 4, - 2, - 45, - 123, - 12, - 56, - 33, - 1, - 4, - 3, - 4, - 7, - 0, - 0, - 0, - 3, - 2, - 0, - 16, - 4, - 2, - 4, - 2, - 0, - 7, - 4, - 5, - 2, - 26, - 2, - 25, - 6, - 11, - 6, - 1, - 16, - 2, - 6, - 17, - 77, - 15, - 3, - 35, - 0, - 1, - 0, - 5, - 1, - 0, - 38, - 16, - 6, - 3, - 12, - 3, - 3, - 3, - 0, - 9, - 3, - 1, - 3, - 5, - 2, - 9, - 0, - 18, - 0, - 25, - 1, - 3, - 32, - 1, - 72, - 46, - 6, - 2, - 7, - 1, - 3, - 14, - 17, - 0, - 28, - 1, - 40, - 13, - 0, - 20, - 15, - 40, - 6, - 38, - 24, - 12, - 43, - 1, - 1, - 9, - 0, - 12, - 6, - 0, - 6, - 2, - 4, - 19, - 3, - 7, - 1, - 48, - 0, - 9, - 5, - 0, - 5, - 6, - 9, - 6, - 10, - 15, - 2, - 11, - 19, - 3, - 9, - 2, - 0, - 1, - 10, - 1, - 27, - 8, - 1, - 3, - 6, - 1, - 14, - 0, - 26, - 0, - 27, - 16, - 3, - 4, - 9, - 6, - 2, - 23, - 9, - 10, - 5, - 25, - 2, - 1, - 6, - 1, - 1, - 48, - 15, - 9, - 15, - 14, - 3, - 4, - 26, - 60, - 29, - 13, - 37, - 21, - 1, - 6, - 4, - 0, - 2, - 11, - 22, - 23, - 16, - 16, - 2, - 2, - 1, - 3, - 0, - 5, - 1, - 6, - 4, - 0, - 0, - 4, - 0, - 0, - 8, - 3, - 0, - 2, - 5, - 0, - 7, - 1, - 7, - 3, - 13, - 2, - 4, - 10, - 3, - 0, - 2, - 31, - 0, - 18, - 3, - 0, - 12, - 10, - 4, - 1, - 0, - 7, - 5, - 7, - 0, - 5, - 4, - 12, - 2, - 22, - 10, - 4, - 2, - 15, - 2, - 8, - 9, - 0, - 23, - 2, - 197, - 51, - 3, - 1, - 1, - 4, - 13, - 4, - 3, - 21, - 4, - 19, - 3, - 10, - 5, - 40, - 0, - 4, - 1, - 1, - 10, - 4, - 1, - 27, - 34, - 7, - 21, - 2, - 17, - 2, - 9, - 6, - 4, - 2, - 3, - 0, - 4, - 2, - 7, - 8, - 2, - 5, - 1, - 15, - 21, - 3, - 4, - 4, - 2, - 2, - 17, - 22, - 1, - 5, - 22, - 4, - 26, - 7, - 0, - 32, - 1, - 11, - 42, - 15, - 4, - 1, - 2, - 5, - 0, - 19, - 3, - 1, - 8, - 6, - 0, - 10, - 1, - 9, - 2, - 13, - 30, - 8, - 2, - 24, - 17, - 19, - 1, - 4, - 4, - 25, - 13, - 0, - 10, - 16, - 11, - 39, - 18, - 8, - 5, - 30, - 82, - 1, - 6, - 8, - 18, - 77, - 11, - 13, - 20, - 75, - 11, - 112, - 78, - 33, - 3, - 0, - 0, - 60, - 17, - 84, - 9, - 1, - 1, - 12, - 30, - 10, - 49, - 5, - 32, - 158, - 178, - 5, - 5, - 6, - 3, - 3, - 1, - 3, - 1, - 4, - 7, - 6, - 19, - 31, - 21, - 0, - 2, - 9, - 5, - 6, - 27, - 4, - 9, - 8, - 1, - 76, - 18, - 12, - 1, - 4, - 0, - 3, - 3, - 6, - 3, - 12, - 2, - 8, - 30, - 16, - 2, - 25, - 1, - 5, - 5, - 4, - 3, - 0, - 6, - 10, - 2, - 3, - 1, - 0, - 5, - 1, - 19, - 3, - 0, - 8, - 1, - 5, - 2, - 6, - 0, - 0, - 0, - 19, - 1, - 2, - 0, - 5, - 1, - 2, - 5, - 1, - 3, - 7, - 0, - 4, - 12, - 7, - 3, - 10, - 22, - 0, - 9, - 5, - 1, - 0, - 2, - 20, - 1, - 1, - 3, - 23, - 30, - 3, - 9, - 9, - 1, - 4, - 191, - 14, - 3, - 15, - 6, - 8, - 50, - 0, - 1, - 0, - 0, - 4, - 0, - 0, - 1, - 0, - 2, - 4, - 2, - 0, - 2, - 3, - 0, - 2, - 0, - 2, - 2, - 8, - 7, - 0, - 1, - 1, - 1, - 3, - 3, - 17, - 11, - 91, - 1, - 9, - 3, - 2, - 13, - 4, - 24, - 15, - 41, - 3, - 13, - 3, - 1, - 20, - 4, - 125, - 29, - 30, - 1, - 0, - 4, - 12, - 2, - 21, - 4, - 5, - 5, - 19, - 11, - 0, - 13, - 11, - 86, - 2, - 18, - 0, - 7, - 1, - 8, - 8, - 2, - 2, - 22, - 1, - 2, - 6, - 5, - 2, - 0, - 1, - 2, - 8, - 0, - 2, - 0, - 5, - 2, - 1, - 0, - 2, - 10, - 2, - 0, - 5, - 9, - 2, - 1, - 2, - 0, - 1, - 0, - 4, - 0, - 0, - 10, - 2, - 5, - 3, - 0, - 6, - 1, - 0, - 1, - 4, - 4, - 33, - 3, - 13, - 17, - 3, - 18, - 6, - 4, - 7, - 1, - 5, - 78, - 0, - 4, - 1, - 13, - 7, - 1, - 8, - 1, - 0, - 35, - 27, - 15, - 3, - 0, - 0, - 0, - 1, - 11, - 5, - 41, - 38, - 15, - 22, - 6, - 14, - 14, - 2, - 1, - 11, - 6, - 20, - 63, - 5, - 8, - 27, - 7, - 11, - 2, - 2, - 40, - 58, - 23, - 50, - 54, - 56, - 293, - 8, - 8, - 1, - 5, - 1, - 14, - 0, - 1, - 12, - 37, - 89, - 8, - 8, - 8, - 2, - 10, - 6, - 0, - 0, - 0, - 4, - 5, - 2, - 1, - 0, - 1, - 1, - 2, - 7, - 0, - 3, - 3, - 0, - 4, - 6, - 0, - 3, - 2, - 19, - 3, - 8, - 0, - 0, - 0, - 4, - 4, - 16, - 0, - 4, - 1, - 5, - 1, - 3, - 0, - 3, - 4, - 6, - 2, - 17, - 10, - 10, - 31, - 6, - 4, - 3, - 6, - 10, - 126, - 7, - 3, - 2, - 2, - 0, - 9, - 0, - 0, - 5, - 20, - 13, - 0, - 15, - 0, - 6, - 0, - 2, - 5, - 8, - 64, - 50, - 3, - 2, - 12, - 2, - 9, - 0, - 0, - 11, - 8, - 20, - 109, - 2, - 18, - 23, - 0, - 0, - 9, - 61, - 3, - 0, - 28, - 41, - 77, - 27, - 19, - 17, - 81, - 5, - 2, - 14, - 5, - 83, - 57, - 252, - 14, - 154, - 263, - 14, - 20, - 8, - 13, - 6, - 57, - 39, - 38, - }; - static ImWchar base_ranges[] = - { - 0x0020, 0x00FF, // Basic Latin + Latin Supplement - 0x3000, 0x30FF, // Punctuations, Hiragana, Katakana - 0x31F0, 0x31FF, // Katakana Phonetic Extensions - 0xFF00, 0xFFEF, // Half-width characters - }; - static bool full_ranges_unpacked = false; - static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(offsets_from_0x4E00) * 2 + 1]; - if (!full_ranges_unpacked) - { - // Unpack - int codepoint = 0x4e00; - memcpy(full_ranges, base_ranges, sizeof(base_ranges)); - ImWchar *dst = full_ranges + IM_ARRAYSIZE(base_ranges); - ; - for (int n = 0; n < IM_ARRAYSIZE(offsets_from_0x4E00); n++, dst += 2) - dst[0] = dst[1] = (ImWchar) (codepoint += (offsets_from_0x4E00[n] + 1)); - dst[0] = 0; - full_ranges_unpacked = true; - } - return &full_ranges[0]; -} - -const ImWchar *ImFontAtlas::GetGlyphRangesCyrillic() -{ - static const ImWchar ranges[] = - { - 0x0020, - 0x00FF, // Basic Latin + Latin Supplement - 0x0400, - 0x052F, // Cyrillic + Cyrillic Supplement - 0x2DE0, - 0x2DFF, // Cyrillic Extended-A - 0xA640, - 0xA69F, // Cyrillic Extended-B - 0, - }; - return &ranges[0]; -} - -const ImWchar *ImFontAtlas::GetGlyphRangesThai() -{ - static const ImWchar ranges[] = - { - 0x0020, - 0x00FF, // Basic Latin - 0x2010, - 0x205E, // Punctuations - 0x0E00, - 0x0E7F, // Thai - 0, - }; - return &ranges[0]; -} + // List of hardcoded separators: .,;!?'" -//----------------------------------------------------------------------------- -// ImFontAtlas::GlyphRangesBuilder -//----------------------------------------------------------------------------- + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" --> "Hello" "World" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + IM_ASSERT(text_end != NULL); + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + const char* next_s; + if (c < 0x80) + next_s = s + 1; + else + next_s = s + ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + if (c == '\r') + { + s = next_s; + continue; + } + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX); + if (ImCharIsBlankW(c)) + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + word_end = s; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width > wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + // +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol). + if (s == text && text < text_end) + return s + 1; + return s; +} -void ImFontAtlas::GlyphRangesBuilder::AddText(const char *text, const char *text_end) +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const { - while (text_end ? (text < text_end) : *text) - { - unsigned int c = 0; - int c_len = ImTextCharFromUtf8(&c, text, text_end); - text += c_len; - if (c_len == 0) - break; - if (c < 0x10000) - AddChar((ImWchar) c); - } + if (!text_end) + text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. + + const float line_height = size; + const float scale = size / FontSize; + + ImVec2 text_size = ImVec2(0, 0); + float line_width = 0.0f; + + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + const char* s = text_begin; + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + continue; + } + } + + // Decode and advance source + const char* prev_s = s; + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; + } + if (c == '\r') + continue; + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX) * scale; + if (line_width + char_width >= max_width) + { + s = prev_s; + break; + } + + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (line_width > 0 || text_size.y == 0.0f) + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; } -void ImFontAtlas::GlyphRangesBuilder::AddRanges(const ImWchar *ranges) +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) const { - for (; ranges[0]; ranges += 2) - for (ImWchar c = ranges[0]; c <= ranges[1]; c++) - AddChar(c); + const ImFontGlyph* glyph = FindGlyph(c); + if (!glyph || !glyph->Visible) + return; + if (glyph->Colored) + col |= ~IM_COL32_A_MASK; + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(x + glyph->X0 * scale, y + glyph->Y0 * scale), ImVec2(x + glyph->X1 * scale, y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); } -void ImFontAtlas::GlyphRangesBuilder::BuildRanges(ImVector *out_ranges) +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const { - for (int n = 0; n < 0x10000; n++) - if (GetBit(n)) - { - out_ranges->push_back((ImWchar) n); - while (n < 0x10000 && GetBit(n + 1)) - n++; - out_ranges->push_back((ImWchar) n); - } - out_ranges->push_back(0); + if (!text_end) + text_end = text_begin + strlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. + + // Align to be pixel perfect + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); + if (y > clip_rect.w) + return; + + const float start_x = x; + const float scale = size / FontSize; + const float line_height = FontSize * scale; + const bool word_wrap_enabled = (wrap_width > 0.0f); + + // Fast-forward to first visible line + const char* s = text_begin; + if (y + line_height < clip_rect.y) + while (y + line_height < clip_rect.y && s < text_end) + { + const char* line_end = (const char*)memchr(s, '\n', text_end - s); + if (word_wrap_enabled) + { + // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPositionA(). + // If the specs for CalcWordWrapPositionA() were reworked to optionally return on \n we could combine both. + // However it is still better than nothing performing the fast-forward! + s = CalcWordWrapPositionA(scale, s, line_end ? line_end : text_end, wrap_width); + s = CalcWordWrapNextLineStartA(s, text_end); + } + else + { + s = line_end ? line_end + 1 : text_end; + } + y += line_height; + } + + // For large text, scan for the last visible line in order to avoid over-reserving in the call to PrimReserve() + // Note that very large horizontal line will still be affected by the issue (e.g. a one megabyte string buffer without a newline will likely crash atm) + if (text_end - s > 10000 && !word_wrap_enabled) + { + const char* s_end = s; + float y_end = y; + while (y_end < clip_rect.w && s_end < text_end) + { + s_end = (const char*)memchr(s_end, '\n', text_end - s_end); + s_end = s_end ? s_end + 1 : text_end; + y_end += line_height; + } + text_end = s_end; + } + if (s == text_end) + return; + + // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) + const int vtx_count_max = (int)(text_end - s) * 4; + const int idx_count_max = (int)(text_end - s) * 6; + const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; + draw_list->PrimReserve(idx_count_max, vtx_count_max); + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_index = draw_list->_VtxCurrentIdx; + + const ImU32 col_untinted = col | ~IM_COL32_A_MASK; + const char* word_wrap_eol = NULL; + + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - start_x)); + + if (s >= word_wrap_eol) + { + x = start_x; + y += line_height; + if (y > clip_rect.w) + break; // break out of main loop + word_wrap_eol = NULL; + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + continue; + } + } + + // Decode and advance source + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + x = start_x; + y += line_height; + if (y > clip_rect.w) + break; // break out of main loop + continue; + } + if (c == '\r') + continue; + } + + const ImFontGlyph* glyph = FindGlyph((ImWchar)c); + if (glyph == NULL) + continue; + + float char_width = glyph->AdvanceX * scale; + if (glyph->Visible) + { + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) + { + if (x1 < clip_rect.x) + { + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; + } + if (y1 < clip_rect.y) + { + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; + } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // Support for untinted glyphs + ImU32 glyph_col = glyph->Colored ? col_untinted : col; + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = glyph_col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = glyph_col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = glyph_col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = glyph_col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + idx_write[0] = (ImDrawIdx)(vtx_index); idx_write[1] = (ImDrawIdx)(vtx_index + 1); idx_write[2] = (ImDrawIdx)(vtx_index + 2); + idx_write[3] = (ImDrawIdx)(vtx_index); idx_write[4] = (ImDrawIdx)(vtx_index + 2); idx_write[5] = (ImDrawIdx)(vtx_index + 3); + vtx_write += 4; + vtx_index += 4; + idx_write += 6; + } + } + } + x += char_width; + } + + // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. + draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data); // Same as calling shrink() + draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + draw_list->_VtxWritePtr = vtx_write; + draw_list->_IdxWritePtr = idx_write; + draw_list->_VtxCurrentIdx = vtx_index; } //----------------------------------------------------------------------------- -// ImFont +// [SECTION] ImGui Internal Render Helpers +//----------------------------------------------------------------------------- +// Vaguely redesigned to stop accessing ImGui global state: +// - RenderArrow() +// - RenderBullet() +// - RenderCheckMark() +// - RenderArrowPointingAt() +// - RenderRectFilledRangeH() +// - RenderRectFilledWithHole() +//----------------------------------------------------------------------------- +// Function in need of a redesign (legacy mess) +// - RenderColorRectWithAlphaCheckerboard() //----------------------------------------------------------------------------- -ImFont::ImFont() +// Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state +void ImGui::RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale) { - Scale = 1.0f; - FallbackChar = (ImWchar) '?'; - DisplayOffset = ImVec2(0.0f, 1.0f); - ClearOutputData(); -} + const float h = draw_list->_Data->FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = pos + ImVec2(h * 0.50f, h * 0.50f * scale); -ImFont::~ImFont() -{ - // Invalidate active font so that the user gets a clear crash instead of a dangling pointer. - // If you want to delete fonts you need to do it between Render() and NewFrame(). - // FIXME-CLEANUP - /* - ImGuiContext& g = *GImGui; - if (g.Font == this) - g.Font = NULL; - */ - ClearOutputData(); + ImVec2 a, b, c; + switch (dir) + { + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + a = ImVec2(+0.000f, +0.750f) * r; + b = ImVec2(-0.866f, -0.750f) * r; + c = ImVec2(+0.866f, -0.750f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + a = ImVec2(+0.750f, +0.000f) * r; + b = ImVec2(-0.750f, +0.866f) * r; + c = ImVec2(-0.750f, -0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_COUNT: + IM_ASSERT(0); + break; + } + draw_list->AddTriangleFilled(center + a, center + b, center + c, col); } -void ImFont::ClearOutputData() +void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) { - FontSize = 0.0f; - Glyphs.clear(); - IndexAdvanceX.clear(); - IndexLookup.clear(); - FallbackGlyph = NULL; - FallbackAdvanceX = 0.0f; - ConfigDataCount = 0; - ConfigData = NULL; - ContainerAtlas = NULL; - Ascent = Descent = 0.0f; - MetricsTotalSurface = 0; + // FIXME-OPT: This should be baked in font. + draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8); } -void ImFont::BuildLookupTable() +void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) { - int max_codepoint = 0; - for (int i = 0; i != Glyphs.Size; i++) - max_codepoint = ImMax(max_codepoint, (int) Glyphs[i].Codepoint); - - IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved - IndexAdvanceX.clear(); - IndexLookup.clear(); - GrowIndex(max_codepoint + 1); - for (int i = 0; i < Glyphs.Size; i++) - { - int codepoint = (int) Glyphs[i].Codepoint; - IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; - IndexLookup[codepoint] = (unsigned short) i; - } - - // Create a glyph to handle TAB - // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) - if (FindGlyph((unsigned short) ' ')) - { - if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times - Glyphs.resize(Glyphs.Size + 1); - ImFontGlyph &tab_glyph = Glyphs.back(); - tab_glyph = *FindGlyph((unsigned short) ' '); - tab_glyph.Codepoint = '\t'; - tab_glyph.AdvanceX *= 4; - IndexAdvanceX[(int) tab_glyph.Codepoint] = (float) tab_glyph.AdvanceX; - IndexLookup[(int) tab_glyph.Codepoint] = (unsigned short) (Glyphs.Size - 1); - } - - FallbackGlyph = NULL; - FallbackGlyph = FindGlyph(FallbackChar); - FallbackAdvanceX = FallbackGlyph ? FallbackGlyph->AdvanceX : 0.0f; - for (int i = 0; i < max_codepoint + 1; i++) - if (IndexAdvanceX[i] < 0.0f) - IndexAdvanceX[i] = FallbackAdvanceX; -} - -void ImFont::SetFallbackChar(ImWchar c) -{ - FallbackChar = c; - BuildLookupTable(); + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness * 0.5f; + pos += ImVec2(thickness * 0.25f, thickness * 0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third * 0.5f; + draw_list->PathLineTo(ImVec2(bx - third, by - third)); + draw_list->PathLineTo(ImVec2(bx, by)); + draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f)); + draw_list->PathStroke(col, 0, thickness); } -void ImFont::GrowIndex(int new_size) +// Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. +void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) { - IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size); - if (new_size <= IndexLookup.Size) - return; - IndexAdvanceX.resize(new_size, -1.0f); - IndexLookup.resize(new_size, (unsigned short) -1); + switch (direction) + { + case ImGuiDir_Left: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Right: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_Up: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Down: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_None: case ImGuiDir_COUNT: break; // Fix warnings + } } -void ImFont::AddGlyph(ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) +static inline float ImAcos01(float x) { - Glyphs.resize(Glyphs.Size + 1); - ImFontGlyph &glyph = Glyphs.back(); - glyph.Codepoint = (ImWchar) codepoint; - glyph.X0 = x0; - glyph.Y0 = y0; - glyph.X1 = x1; - glyph.Y1 = y1; - glyph.U0 = u0; - glyph.V0 = v0; - glyph.U1 = u1; - glyph.V1 = v1; - glyph.AdvanceX = advance_x + ConfigData->GlyphExtraSpacing.x; // Bake spacing into AdvanceX - - if (ConfigData->PixelSnapH) - glyph.AdvanceX = (float) (int) (glyph.AdvanceX + 0.5f); - - // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) - MetricsTotalSurface += (int) ((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + 1.99f) * (int) ((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + 1.99f); + if (x <= 0.0f) return IM_PI * 0.5f; + if (x >= 1.0f) return 0.0f; + return ImAcos(x); + //return (-0.69813170079773212f * x * x - 0.87266462599716477f) * x + 1.5707963267948966f; // Cheap approximation, may be enough for what we do. } -void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) +// FIXME: Cleanup and move code to ImDrawList. +void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) { - IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. - int index_size = IndexLookup.Size; - - if (dst < index_size && IndexLookup.Data[dst] == (unsigned short) -1 && !overwrite_dst) // 'dst' already exists - return; - if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op - return; - - GrowIndex(dst + 1); - IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (unsigned short) -1; - IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; -} - -const ImFontGlyph *ImFont::FindGlyph(ImWchar c) const -{ - if (c < IndexLookup.Size) - { - const unsigned short i = IndexLookup[c]; - if (i != (unsigned short) -1) - return &Glyphs.Data[i]; - } - return FallbackGlyph; -} - -const char *ImFont::CalcWordWrapPositionA(float scale, const char *text, const char *text_end, float wrap_width) const -{ - // Simple word-wrapping for English, not full-featured. Please submit failing cases! - // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) - - // For references, possible wrap point marked with ^ - // "aaa bbb, ccc,ddd. eee fff. ggg!" - // ^ ^ ^ ^ ^__ ^ ^ - - // List of hardcoded separators: .,;!?'" - - // Skip extra blanks after a line returns (that includes not counting them in width computation) - // e.g. "Hello world" --> "Hello" "World" - - // Cut words that cannot possibly fit within one line. - // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" - - float line_width = 0.0f; - float word_width = 0.0f; - float blank_width = 0.0f; - wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters - - const char *word_end = text; - const char *prev_word_end = NULL; - bool inside_word = true; - - const char *s = text; - while (s < text_end) - { - unsigned int c = (unsigned int) *s; - const char *next_s; - if (c < 0x80) - next_s = s + 1; - else - next_s = s + ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) - break; - - if (c < 32) - { - if (c == '\n') - { - line_width = word_width = blank_width = 0.0f; - inside_word = true; - s = next_s; - continue; - } - if (c == '\r') - { - s = next_s; - continue; - } - } - - const float char_width = ((int) c < IndexAdvanceX.Size ? IndexAdvanceX[(int) c] : FallbackAdvanceX); - if (ImCharIsSpace(c)) - { - if (inside_word) - { - line_width += blank_width; - blank_width = 0.0f; - word_end = s; - } - blank_width += char_width; - inside_word = false; - } - else - { - word_width += char_width; - if (inside_word) - { - word_end = next_s; - } - else - { - prev_word_end = word_end; - line_width += word_width + blank_width; - word_width = blank_width = 0.0f; - } - - // Allow wrapping after punctuation. - inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"'); - } - - // We ignore blank width at the end of the line (they can be skipped) - if (line_width + word_width >= wrap_width) - { - // Words that cannot possibly fit within an entire line will be cut anywhere. - if (word_width < wrap_width) - s = prev_word_end ? prev_word_end : word_end; - break; - } - - s = next_s; - } - - return s; -} - -ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char *text_begin, const char *text_end, const char **remaining) const -{ - if (!text_end) - text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. - - const float line_height = size; - const float scale = size / FontSize; - - ImVec2 text_size = ImVec2(0, 0); - float line_width = 0.0f; - - const bool word_wrap_enabled = (wrap_width > 0.0f); - const char *word_wrap_eol = NULL; - - const char *s = text_begin; - while (s < text_end) - { - if (word_wrap_enabled) - { - // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. - if (!word_wrap_eol) - { - word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); - if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. - word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below - } - - if (s >= word_wrap_eol) - { - if (text_size.x < line_width) - text_size.x = line_width; - text_size.y += line_height; - line_width = 0.0f; - word_wrap_eol = NULL; - - // Wrapping skips upcoming blanks - while (s < text_end) - { - const char c = *s; - if (ImCharIsSpace(c)) - { - s++; - } - else if (c == '\n') - { - s++; - break; - } - else - { - break; - } - } - continue; - } - } - - // Decode and advance source - const char *prev_s = s; - unsigned int c = (unsigned int) *s; - if (c < 0x80) - { - s += 1; - } - else - { - s += ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) // Malformed UTF-8? - break; - } - - if (c < 32) - { - if (c == '\n') - { - text_size.x = ImMax(text_size.x, line_width); - text_size.y += line_height; - line_width = 0.0f; - continue; - } - if (c == '\r') - continue; - } - - const float char_width = ((int) c < IndexAdvanceX.Size ? IndexAdvanceX[(int) c] : FallbackAdvanceX) * scale; - if (line_width + char_width >= max_width) - { - s = prev_s; - break; - } - - line_width += char_width; - } - - if (text_size.x < line_width) - text_size.x = line_width; - - if (line_width > 0 || text_size.y == 0.0f) - text_size.y += line_height; - - if (remaining) - *remaining = s; - - return text_size; -} - -void ImFont::RenderChar(ImDrawList *draw_list, float size, ImVec2 pos, ImU32 col, unsigned short c) const -{ - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') // Match behavior of RenderText(), those 4 codepoints are hard-coded. - return; - if (const ImFontGlyph *glyph = FindGlyph(c)) - { - float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; - pos.x = (float) (int) pos.x + DisplayOffset.x; - pos.y = (float) (int) pos.y + DisplayOffset.y; - draw_list->PrimReserve(6, 4); - draw_list->PrimRectUV(ImVec2(pos.x + glyph->X0 * scale, pos.y + glyph->Y0 * scale), ImVec2(pos.x + glyph->X1 * scale, pos.y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); - } -} - -void ImFont::RenderText(ImDrawList *draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4 &clip_rect, const char *text_begin, const char *text_end, float wrap_width, bool cpu_fine_clip) const -{ - if (!text_end) - text_end = text_begin + strlen(text_begin); // ImGui functions generally already provides a valid text_end, so this is merely to handle direct calls. - - // Align to be pixel perfect - pos.x = (float) (int) pos.x + DisplayOffset.x; - pos.y = (float) (int) pos.y + DisplayOffset.y; - float x = pos.x; - float y = pos.y; - if (y > clip_rect.w) - return; - - const float scale = size / FontSize; - const float line_height = FontSize * scale; - const bool word_wrap_enabled = (wrap_width > 0.0f); - const char *word_wrap_eol = NULL; - - // Skip non-visible lines - const char *s = text_begin; - if (!word_wrap_enabled && y + line_height < clip_rect.y) - while (s < text_end && *s != '\n') // Fast-forward to next line - s++; - - // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) - const int vtx_count_max = (int) (text_end - s) * 4; - const int idx_count_max = (int) (text_end - s) * 6; - const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; - draw_list->PrimReserve(idx_count_max, vtx_count_max); - - ImDrawVert *vtx_write = draw_list->_VtxWritePtr; - ImDrawIdx *idx_write = draw_list->_IdxWritePtr; - unsigned int vtx_current_idx = draw_list->_VtxCurrentIdx; - - while (s < text_end) - { - if (word_wrap_enabled) - { - // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. - if (!word_wrap_eol) - { - word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x)); - if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. - word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below - } - - if (s >= word_wrap_eol) - { - x = pos.x; - y += line_height; - word_wrap_eol = NULL; - - // Wrapping skips upcoming blanks - while (s < text_end) - { - const char c = *s; - if (ImCharIsSpace(c)) - { - s++; - } - else if (c == '\n') - { - s++; - break; - } - else - { - break; - } - } - continue; - } - } - - // Decode and advance source - unsigned int c = (unsigned int) *s; - if (c < 0x80) - { - s += 1; - } - else - { - s += ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) // Malformed UTF-8? - break; - } - - if (c < 32) - { - if (c == '\n') - { - x = pos.x; - y += line_height; - - if (y > clip_rect.w) - break; - if (!word_wrap_enabled && y + line_height < clip_rect.y) - while (s < text_end && *s != '\n') // Fast-forward to next line - s++; - continue; - } - if (c == '\r') - continue; - } - - float char_width = 0.0f; - if (const ImFontGlyph *glyph = FindGlyph((unsigned short) c)) - { - char_width = glyph->AdvanceX * scale; - - // Arbitrarily assume that both space and tabs are empty glyphs as an optimization - if (c != ' ' && c != '\t') - { - // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w - float x1 = x + glyph->X0 * scale; - float x2 = x + glyph->X1 * scale; - float y1 = y + glyph->Y0 * scale; - float y2 = y + glyph->Y1 * scale; - if (x1 <= clip_rect.z && x2 >= clip_rect.x) - { - // Render a character - float u1 = glyph->U0; - float v1 = glyph->V0; - float u2 = glyph->U1; - float v2 = glyph->V1; - - // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. - if (cpu_fine_clip) - { - if (x1 < clip_rect.x) - { - u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); - x1 = clip_rect.x; - } - if (y1 < clip_rect.y) - { - v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); - y1 = clip_rect.y; - } - if (x2 > clip_rect.z) - { - u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); - x2 = clip_rect.z; - } - if (y2 > clip_rect.w) - { - v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); - y2 = clip_rect.w; - } - if (y1 >= y2) - { - x += char_width; - continue; - } - } - - // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: - { - idx_write[0] = (ImDrawIdx) (vtx_current_idx); - idx_write[1] = (ImDrawIdx) (vtx_current_idx + 1); - idx_write[2] = (ImDrawIdx) (vtx_current_idx + 2); - idx_write[3] = (ImDrawIdx) (vtx_current_idx); - idx_write[4] = (ImDrawIdx) (vtx_current_idx + 2); - idx_write[5] = (ImDrawIdx) (vtx_current_idx + 3); - vtx_write[0].pos.x = x1; - vtx_write[0].pos.y = y1; - vtx_write[0].col = col; - vtx_write[0].uv.x = u1; - vtx_write[0].uv.y = v1; - vtx_write[1].pos.x = x2; - vtx_write[1].pos.y = y1; - vtx_write[1].col = col; - vtx_write[1].uv.x = u2; - vtx_write[1].uv.y = v1; - vtx_write[2].pos.x = x2; - vtx_write[2].pos.y = y2; - vtx_write[2].col = col; - vtx_write[2].uv.x = u2; - vtx_write[2].uv.y = v2; - vtx_write[3].pos.x = x1; - vtx_write[3].pos.y = y2; - vtx_write[3].col = col; - vtx_write[3].uv.x = u1; - vtx_write[3].uv.y = v2; - vtx_write += 4; - vtx_current_idx += 4; - idx_write += 6; - } - } - } - } - - x += char_width; - } - - // Give back unused vertices - draw_list->VtxBuffer.resize((int) (vtx_write - draw_list->VtxBuffer.Data)); - draw_list->IdxBuffer.resize((int) (idx_write - draw_list->IdxBuffer.Data)); - draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); - draw_list->_VtxWritePtr = vtx_write; - draw_list->_IdxWritePtr = idx_write; - draw_list->_VtxCurrentIdx = (unsigned int) draw_list->VtxBuffer.Size; + if (x_end_norm == x_start_norm) + return; + if (x_start_norm > x_end_norm) + ImSwap(x_start_norm, x_end_norm); + + ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); + ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); + if (rounding == 0.0f) + { + draw_list->AddRectFilled(p0, p1, col, 0.0f); + return; + } + + rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); + const float inv_rounding = 1.0f / rounding; + const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); + const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); + const float half_pi = IM_PI * 0.5f; // We will == compare to this because we know this is the exact value ImAcos01 can return. + const float x0 = ImMax(p0.x, rect.Min.x + rounding); + if (arc0_b == arc0_e) + { + draw_list->PathLineTo(ImVec2(x0, p1.y)); + draw_list->PathLineTo(ImVec2(x0, p0.y)); + } + else if (arc0_b == 0.0f && arc0_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL + draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR + } + else + { + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e); // TR + } + if (p1.x > rect.Min.x + rounding) + { + const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); + const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); + const float x1 = ImMin(p1.x, rect.Max.x - rounding); + if (arc1_b == arc1_e) + { + draw_list->PathLineTo(ImVec2(x1, p0.y)); + draw_list->PathLineTo(ImVec2(x1, p1.y)); + } + else if (arc1_b == 0.0f && arc1_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR + draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR + } + else + { + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e); // BR + } + } + draw_list->PathFillConvex(col); } -//----------------------------------------------------------------------------- -// Internals Drawing Helpers -//----------------------------------------------------------------------------- - -static inline float ImAcos01(float x) +void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding) { - if (x <= 0.0f) - return IM_PI * 0.5f; - if (x >= 1.0f) - return 0.0f; - return acosf(x); - // return (-0.69813170079773212f * x * x - 0.87266462599716477f) * x + 1.5707963267948966f; // Cheap approximation, may be enough for what we do. + const bool fill_L = (inner.Min.x > outer.Min.x); + const bool fill_R = (inner.Max.x < outer.Max.x); + const bool fill_U = (inner.Min.y > outer.Min.y); + const bool fill_D = (inner.Max.y < outer.Max.y); + if (fill_L) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomLeft)); + if (fill_R) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopRight) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_U) draw_list->AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersTopRight)); + if (fill_D) draw_list->AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersBottomLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_L && fill_U) draw_list->AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopLeft); + if (fill_R && fill_U) draw_list->AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopRight); + if (fill_L && fill_D) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomLeft); + if (fill_R && fill_D) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomRight); } -// FIXME: Cleanup and move code to ImDrawList. -void ImGui::RenderRectFilledRangeH(ImDrawList *draw_list, const ImRect &rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) -{ - if (x_end_norm == x_start_norm) - return; - if (x_start_norm > x_end_norm) - ImSwap(x_start_norm, x_end_norm); - - ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); - ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); - if (rounding == 0.0f) - { - draw_list->AddRectFilled(p0, p1, col, 0.0f); - return; - } - - rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); - const float inv_rounding = 1.0f / rounding; - const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); - const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); - const float x0 = ImMax(p0.x, rect.Min.x + rounding); - if (arc0_b == arc0_e) - { - draw_list->PathLineTo(ImVec2(x0, p1.y)); - draw_list->PathLineTo(ImVec2(x0, p0.y)); - } - else if (arc0_b == 0.0f && arc0_e == IM_PI * 0.5f) - { - draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL - draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR - } - else - { - draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL - draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR - } - if (p1.x > rect.Min.x + rounding) - { - const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); - const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); - const float x1 = ImMin(p1.x, rect.Max.x - rounding); - if (arc1_b == arc1_e) - { - draw_list->PathLineTo(ImVec2(x1, p0.y)); - draw_list->PathLineTo(ImVec2(x1, p1.y)); - } - else if (arc1_b == 0.0f && arc1_e == IM_PI * 0.5f) - { - draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR - draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR - } - else - { - draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR - draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR - } - } - draw_list->PathFillConvex(col); +// Helper for ColorPicker4() +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. +// FIXME: uses ImGui::GetColorU32 +void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, ImDrawFlags flags) +{ + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags = ImDrawFlags_RoundCornersDefault_; + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col)); + draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; + if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; } + + // Combine flags + cell_flags = (flags == ImDrawFlags_RoundCornersNone || cell_flags == ImDrawFlags_RoundCornersNone) ? ImDrawFlags_RoundCornersNone : (cell_flags & flags); + draw_list->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding, cell_flags); + } + } + } + else + { + draw_list->AddRectFilled(p_min, p_max, col, rounding, flags); + } } //----------------------------------------------------------------------------- -// DEFAULT FONT DATA +// [SECTION] Decompression code //----------------------------------------------------------------------------- -// Compressed with stb_compress() then converted to a C array. +// Compressed with stb_compress() then converted to a C array and encoded as base85. // Use the program in misc/fonts/binary_to_compressed_c.cpp to create the array from a TTF file. +// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. // Decompression from stb.h (public domain) by Sean Barrett https://github.com/nothings/stb/blob/master/stb.h //----------------------------------------------------------------------------- -static unsigned int stb_decompress_length(unsigned char *input) +static unsigned int stb_decompress_length(const unsigned char *input) { - return (input[8] << 24) + (input[9] << 16) + (input[10] << 8) + input[11]; + return (input[8] << 24) + (input[9] << 16) + (input[10] << 8) + input[11]; } -static unsigned char *stb__barrier, *stb__barrier2, *stb__barrier3, *stb__barrier4; +static unsigned char *stb__barrier_out_e, *stb__barrier_out_b; +static const unsigned char *stb__barrier_in_b; static unsigned char *stb__dout; -static void stb__match(unsigned char *data, unsigned int length) -{ - // INVERSE of memmove... write each byte before copying the next... - IM_ASSERT(stb__dout + length <= stb__barrier); - if (stb__dout + length > stb__barrier) - { - stb__dout += length; - return; - } - if (data < stb__barrier4) - { - stb__dout = stb__barrier + 1; - return; - } - while (length--) - *stb__dout++ = *data++; -} - -static void stb__lit(unsigned char *data, unsigned int length) -{ - IM_ASSERT(stb__dout + length <= stb__barrier); - if (stb__dout + length > stb__barrier) - { - stb__dout += length; - return; - } - if (data < stb__barrier2) - { - stb__dout = stb__barrier + 1; - return; - } - memcpy(stb__dout, data, length); - stb__dout += length; -} - -#define stb__in2(x) ((i[x] << 8) + i[(x) + 1]) -#define stb__in3(x) ((i[x] << 16) + stb__in2((x) + 1)) -#define stb__in4(x) ((i[x] << 24) + stb__in3((x) + 1)) - -static unsigned char *stb_decompress_token(unsigned char *i) -{ - if (*i >= 0x20) - { // use fewer if's for cases that expand small - if (*i >= 0x80) - stb__match(stb__dout - i[1] - 1, i[0] - 0x80 + 1), i += 2; - else if (*i >= 0x40) - stb__match(stb__dout - (stb__in2(0) - 0x4000 + 1), i[2] + 1), i += 3; - else /* *i >= 0x20 */ - stb__lit(i + 1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); - } - else - { // more ifs for cases that expand large, since overhead is amortized - if (*i >= 0x18) - stb__match(stb__dout - (stb__in3(0) - 0x180000 + 1), i[3] + 1), i += 4; - else if (*i >= 0x10) - stb__match(stb__dout - (stb__in3(0) - 0x100000 + 1), stb__in2(3) + 1), i += 5; - else if (*i >= 0x08) - stb__lit(i + 2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); - else if (*i == 0x07) - stb__lit(i + 3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); - else if (*i == 0x06) - stb__match(stb__dout - (stb__in3(1) + 1), i[4] + 1), i += 5; - else if (*i == 0x04) - stb__match(stb__dout - (stb__in3(1) + 1), stb__in2(4) + 1), i += 6; - } - return i; +static void stb__match(const unsigned char *data, unsigned int length) +{ + // INVERSE of memmove... write each byte before copying the next... + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_out_b) { stb__dout = stb__barrier_out_e+1; return; } + while (length--) *stb__dout++ = *data++; +} + +static void stb__lit(const unsigned char *data, unsigned int length) +{ + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_in_b) { stb__dout = stb__barrier_out_e+1; return; } + memcpy(stb__dout, data, length); + stb__dout += length; +} + +#define stb__in2(x) ((i[x] << 8) + i[(x)+1]) +#define stb__in3(x) ((i[x] << 16) + stb__in2((x)+1)) +#define stb__in4(x) ((i[x] << 24) + stb__in3((x)+1)) + +static const unsigned char *stb_decompress_token(const unsigned char *i) +{ + if (*i >= 0x20) { // use fewer if's for cases that expand small + if (*i >= 0x80) stb__match(stb__dout-i[1]-1, i[0] - 0x80 + 1), i += 2; + else if (*i >= 0x40) stb__match(stb__dout-(stb__in2(0) - 0x4000 + 1), i[2]+1), i += 3; + else /* *i >= 0x20 */ stb__lit(i+1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); + } else { // more ifs for cases that expand large, since overhead is amortized + if (*i >= 0x18) stb__match(stb__dout-(stb__in3(0) - 0x180000 + 1), i[3]+1), i += 4; + else if (*i >= 0x10) stb__match(stb__dout-(stb__in3(0) - 0x100000 + 1), stb__in2(3)+1), i += 5; + else if (*i >= 0x08) stb__lit(i+2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); + else if (*i == 0x07) stb__lit(i+3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); + else if (*i == 0x06) stb__match(stb__dout-(stb__in3(1)+1), i[4]+1), i += 5; + else if (*i == 0x04) stb__match(stb__dout-(stb__in3(1)+1), stb__in2(4)+1), i += 6; + } + return i; } static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, unsigned int buflen) { - const unsigned long ADLER_MOD = 65521; - unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; - unsigned long blocklen, i; - - blocklen = buflen % 5552; - while (buflen) - { - for (i = 0; i + 7 < blocklen; i += 8) - { - s1 += buffer[0], s2 += s1; - s1 += buffer[1], s2 += s1; - s1 += buffer[2], s2 += s1; - s1 += buffer[3], s2 += s1; - s1 += buffer[4], s2 += s1; - s1 += buffer[5], s2 += s1; - s1 += buffer[6], s2 += s1; - s1 += buffer[7], s2 += s1; - - buffer += 8; - } - - for (; i < blocklen; ++i) - s1 += *buffer++, s2 += s1; - - s1 %= ADLER_MOD, s2 %= ADLER_MOD; - buflen -= blocklen; - blocklen = 5552; - } - return (unsigned int) (s2 << 16) + (unsigned int) s1; -} - -static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsigned int length) -{ - unsigned int olen; - if (stb__in4(0) != 0x57bC0000) - return 0; - if (stb__in4(4) != 0) - return 0; // error! stream is > 4GB - olen = stb_decompress_length(i); - stb__barrier2 = i; - stb__barrier3 = i + length; - stb__barrier = output + olen; - stb__barrier4 = output; - i += 16; - - stb__dout = output; - for (;;) - { - unsigned char *old_i = i; - i = stb_decompress_token(i); - if (i == old_i) - { - if (*i == 0x05 && i[1] == 0xfa) - { - IM_ASSERT(stb__dout == output + olen); - if (stb__dout != output + olen) - return 0; - if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) - return 0; - return olen; - } - else - { - IM_ASSERT(0); /* NOTREACHED */ - return 0; - } - } - IM_ASSERT(stb__dout <= output + olen); - if (stb__dout > output + olen) - return 0; - } + const unsigned long ADLER_MOD = 65521; + unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; + unsigned long blocklen = buflen % 5552; + + unsigned long i; + while (buflen) { + for (i=0; i + 7 < blocklen; i += 8) { + s1 += buffer[0], s2 += s1; + s1 += buffer[1], s2 += s1; + s1 += buffer[2], s2 += s1; + s1 += buffer[3], s2 += s1; + s1 += buffer[4], s2 += s1; + s1 += buffer[5], s2 += s1; + s1 += buffer[6], s2 += s1; + s1 += buffer[7], s2 += s1; + + buffer += 8; + } + + for (; i < blocklen; ++i) + s1 += *buffer++, s2 += s1; + + s1 %= ADLER_MOD, s2 %= ADLER_MOD; + buflen -= blocklen; + blocklen = 5552; + } + return (unsigned int)(s2 << 16) + (unsigned int)s1; +} + +static unsigned int stb_decompress(unsigned char *output, const unsigned char *i, unsigned int /*length*/) +{ + if (stb__in4(0) != 0x57bC0000) return 0; + if (stb__in4(4) != 0) return 0; // error! stream is > 4GB + const unsigned int olen = stb_decompress_length(i); + stb__barrier_in_b = i; + stb__barrier_out_e = output + olen; + stb__barrier_out_b = output; + i += 16; + + stb__dout = output; + for (;;) { + const unsigned char *old_i = i; + i = stb_decompress_token(i); + if (i == old_i) { + if (*i == 0x05 && i[1] == 0xfa) { + IM_ASSERT(stb__dout == output + olen); + if (stb__dout != output + olen) return 0; + if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) + return 0; + return olen; + } else { + IM_ASSERT(0); /* NOTREACHED */ + return 0; + } + } + IM_ASSERT(stb__dout <= output + olen); + if (stb__dout > output + olen) + return 0; + } } +//----------------------------------------------------------------------------- +// [SECTION] Default font data (ProggyClean.ttf) //----------------------------------------------------------------------------- // ProggyClean.ttf // Copyright (c) 2004, 2005 Tristan Grimmer -// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) -// Download and more information at http://upperbounds.net +// MIT license (see License.txt in http://www.proggyfonts.net/index.php?menu=download) +// Download and more information at http://www.proggyfonts.net or http://upperboundsinteractive.com/fonts.php //----------------------------------------------------------------------------- // File: 'ProggyClean.ttf' (41208 bytes) -// Exported using binary_to_compressed_c.cpp +// Exported using misc/fonts/binary_to_compressed_c.cpp (with compression + base85 string encoding). +// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. //----------------------------------------------------------------------------- static const char proggy_clean_ttf_compressed_data_base85[11980 + 1] = "7])#######hV0qs'/###[),##/l:$#Q6>##5[n42>c-TH`->>#/e>11NNV=Bv(*:.F?uu#(gRU.o0XGH`$vhLG1hxt9?W`#,5LsCp#-i>.r$<$6pD>Lb';9Crc6tgXmKVeU2cD4Eo3R/" @@ -5093,7 +4632,9 @@ static const char proggy_clean_ttf_compressed_data_base85[11980 + 1] = "GT4CPGT4CPGT4CPGT4CPGT4CPGT4CP-qekC`.9kEg^+F$kwViFJTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5o,^<-28ZI'O?;xp" "O?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xp;7q-#lLYI:xvD=#"; -static const char *GetDefaultCompressedFontDataTTFBase85() +static const char* GetDefaultCompressedFontDataTTFBase85() { - return proggy_clean_ttf_compressed_data_base85; + return proggy_clean_ttf_compressed_data_base85; } + +#endif // #ifndef IMGUI_DISABLE diff --git a/attachments/simple_engine/imgui/imgui_internal.h b/attachments/simple_engine/imgui/imgui_internal.h index d10eebb1c..a99cb2652 100644 --- a/attachments/simple_engine/imgui/imgui_internal.h +++ b/attachments/simple_engine/imgui/imgui_internal.h @@ -1,1549 +1,3722 @@ -// dear imgui, v1.60 WIP -// (internals) - -// You may use this file to debug, understand or extend ImGui features but we don't provide any guarantee of forward compatibility! -// Set: -// #define IMGUI_DEFINE_MATH_OPERATORS -// To implement maths operators for ImVec2 (disabled by default to not collide with using IM_VEC2_CLASS_EXTRA along with your own math types+operators) +// dear imgui, v1.91.1 +// (internal structures/api) + +// You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. + +/* + +Index of this file: + +// [SECTION] Header mess +// [SECTION] Forward declarations +// [SECTION] Context pointer +// [SECTION] STB libraries includes +// [SECTION] Macros +// [SECTION] Generic helpers +// [SECTION] ImDrawList support +// [SECTION] Data types support +// [SECTION] Widgets support: flags, enums, data structures +// [SECTION] Popup support +// [SECTION] Inputs support +// [SECTION] Clipper support +// [SECTION] Navigation support +// [SECTION] Typing-select support +// [SECTION] Columns support +// [SECTION] Box-select support +// [SECTION] Multi-select support +// [SECTION] Docking support +// [SECTION] Viewport support +// [SECTION] Settings support +// [SECTION] Localization support +// [SECTION] Metrics, Debug tools +// [SECTION] Generic context hooks +// [SECTION] ImGuiContext (main imgui context) +// [SECTION] ImGuiWindowTempData, ImGuiWindow +// [SECTION] Tab bar, Tab item support +// [SECTION] Table support +// [SECTION] ImGui internal API +// [SECTION] ImFontAtlas internal API +// [SECTION] Test Engine specific hooks (imgui_test_engine) + +*/ #pragma once +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- #ifndef IMGUI_VERSION -# error Must include imgui.h before imgui_internal.h +#include "imgui.h" #endif -#include // INT_MIN, INT_MAX -#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf -#include // FILE* +#include // FILE*, sscanf +#include // NULL, malloc, free, qsort, atoi, atof +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // INT_MIN, INT_MAX +// Enable SSE intrinsics if available +#if (defined __SSE__ || defined __x86_64__ || defined _M_X64 || (defined(_M_IX86_FP) && (_M_IX86_FP >= 1))) && !defined(IMGUI_DISABLE_SSE) +#define IMGUI_ENABLE_SSE +#include +#endif + +// Visual Studio warnings #ifdef _MSC_VER -# pragma warning(push) -# pragma warning(disable : 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (push) +#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (disable: 26812) // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloor() +#pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#pragma clang diagnostic ignored "-Wdouble-promotion" +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#endif + +// In 1.89.4, we moved the implementation of "courtesy maths operators" from imgui_internal.h in imgui.h +// As they are frequently requested, we do not want to encourage to many people using imgui_internal.h +#if defined(IMGUI_DEFINE_MATH_OPERATORS) && !defined(IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED) +#error Please '#define IMGUI_DEFINE_MATH_OPERATORS' _BEFORE_ including imgui.h! +#endif + +// Legacy defines +#ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#endif +#ifdef IMGUI_DISABLE_MATH_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS #endif -#ifdef __clang__ -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h -# pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h -# pragma clang diagnostic ignored "-Wold-style-cast" +// Enable stb_truetype by default unless FreeType is enabled. +// You can compile with both by defining both IMGUI_ENABLE_FREETYPE and IMGUI_ENABLE_STB_TRUETYPE together. +#ifndef IMGUI_ENABLE_FREETYPE +#define IMGUI_ENABLE_STB_TRUETYPE #endif //----------------------------------------------------------------------------- -// Forward Declarations +// [SECTION] Forward declarations //----------------------------------------------------------------------------- -struct ImRect; -struct ImGuiColMod; -struct ImGuiStyleMod; -struct ImGuiGroupData; -struct ImGuiMenuColumns; -struct ImGuiDrawContext; -struct ImGuiTextEditState; -struct ImGuiPopupRef; -struct ImGuiWindow; -struct ImGuiWindowSettings; - -typedef int ImGuiLayoutType; // enum: horizontal or vertical // enum ImGuiLayoutType_ -typedef int ImGuiButtonFlags; // flags: for ButtonEx(), ButtonBehavior() // enum ImGuiButtonFlags_ -typedef int ImGuiItemFlags; // flags: for PushItemFlag() // enum ImGuiItemFlags_ -typedef int ImGuiItemStatusFlags; // flags: storage for DC.LastItemXXX // enum ImGuiItemStatusFlags_ -typedef int ImGuiNavHighlightFlags; // flags: for RenderNavHighlight() // enum ImGuiNavHighlightFlags_ -typedef int ImGuiNavDirSourceFlags; // flags: for GetNavInputAmount2d() // enum ImGuiNavDirSourceFlags_ -typedef int ImGuiSeparatorFlags; // flags: for Separator() - internal // enum ImGuiSeparatorFlags_ -typedef int ImGuiSliderFlags; // flags: for SliderBehavior() // enum ImGuiSliderFlags_ +struct ImBitVector; // Store 1-bit per value +struct ImRect; // An axis-aligned rectangle (2 points) +struct ImDrawDataBuilder; // Helper to build a ImDrawData instance +struct ImDrawListSharedData; // Data shared between all ImDrawList instances +struct ImGuiBoxSelectState; // Box-selection state (currently used by multi-selection, could potentially be used by others) +struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it +struct ImGuiContext; // Main Dear ImGui context +struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine +struct ImGuiDataVarInfo; // Variable information (e.g. to access style variables from an enum) +struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() +struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box +struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id +struct ImGuiLastItemData; // Status storage for last submitted items +struct ImGuiLocEntry; // A localization entry. +struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only +struct ImGuiMultiSelectState; // Multi-selection persistent state (for focused selection). +struct ImGuiMultiSelectTempData; // Multi-selection temporary state (while traversing). +struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result +struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions +struct ImGuiNextWindowData; // Storage for SetNextWindow** functions +struct ImGuiNextItemData; // Storage for SetNextItem** functions +struct ImGuiOldColumnData; // Storage data for a single column for legacy Columns() api +struct ImGuiOldColumns; // Storage data for a columns set for legacy Columns() api +struct ImGuiPopupData; // Storage for current popup stack +struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file +struct ImGuiStackSizes; // Storage of stack sizes for debugging/asserting +struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it +struct ImGuiTabBar; // Storage for a tab bar +struct ImGuiTabItem; // Storage for a tab item (within a tab bar) +struct ImGuiTable; // Storage for a table +struct ImGuiTableHeaderData; // Storage for TableAngledHeadersRow() +struct ImGuiTableColumn; // Storage for one column of a table +struct ImGuiTableInstanceData; // Storage for one instance of a same table +struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. +struct ImGuiTableSettings; // Storage for a table .ini settings +struct ImGuiTableColumnsSettings; // Storage for a column .ini settings +struct ImGuiTreeNodeStackData; // Temporary storage for TreeNode(). +struct ImGuiTypingSelectState; // Storage for GetTypingSelectRequest() +struct ImGuiTypingSelectRequest; // Storage for GetTypingSelectRequest() (aimed to be public) +struct ImGuiWindow; // Storage for one window +struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) +struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) + +// Enumerations +// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. +enum ImGuiLocKey : int; // -> enum ImGuiLocKey // Enum: a localization entry for translation. +typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical + +// Flags +typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) +typedef int ImGuiDebugLogFlags; // -> enum ImGuiDebugLogFlags_ // Flags: for ShowDebugLogWindow(), g.DebugLogFlags +typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow(); +typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for g.LastItemData.StatusFlags +typedef int ImGuiOldColumnFlags; // -> enum ImGuiOldColumnFlags_ // Flags: for BeginColumns() +typedef int ImGuiNavHighlightFlags; // -> enum ImGuiNavHighlightFlags_ // Flags: for RenderNavHighlight() +typedef int ImGuiNavMoveFlags; // -> enum ImGuiNavMoveFlags_ // Flags: for navigation requests +typedef int ImGuiNextItemDataFlags; // -> enum ImGuiNextItemDataFlags_ // Flags: for SetNextItemXXX() functions +typedef int ImGuiNextWindowDataFlags; // -> enum ImGuiNextWindowDataFlags_// Flags: for SetNextWindowXXX() functions +typedef int ImGuiScrollFlags; // -> enum ImGuiScrollFlags_ // Flags: for ScrollToItem() and navigation requests +typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // Flags: for SeparatorEx() +typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() +typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() +typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() +typedef int ImGuiWindowRefreshFlags; // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy() + +typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); + +//----------------------------------------------------------------------------- +// [SECTION] Context pointer +// See implementation of this variable in imgui.cpp for comments and details. +//----------------------------------------------------------------------------- + +#ifndef GImGui +extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer +#endif //------------------------------------------------------------------------- -// STB libraries +// [SECTION] STB libraries includes //------------------------------------------------------------------------- -namespace ImGuiStb +namespace ImStb { -#undef STB_TEXTEDIT_STRING -#undef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_STRING ImGuiTextEditState -#define STB_TEXTEDIT_CHARTYPE ImWchar -#define STB_TEXTEDIT_GETWIDTH_NEWLINE -1.0f -#include "stb_textedit.h" +#undef IMSTB_TEXTEDIT_STRING +#undef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_STRING ImGuiInputTextState +#define IMSTB_TEXTEDIT_CHARTYPE ImWchar +#define IMSTB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 +#include "imstb_textedit.h" -} // namespace ImGuiStb +} // namespace ImStb //----------------------------------------------------------------------------- -// Context +// [SECTION] Macros //----------------------------------------------------------------------------- -#ifndef GImGui -extern IMGUI_API ImGuiContext *GImGui; // Current implicit ImGui context pointer +// Debug Printing Into TTY +// (since IMGUI_VERSION_NUM >= 18729: IMGUI_DEBUG_LOG was reworked into IMGUI_DEBUG_PRINTF (and removed framecount from it). If you were using a #define IMGUI_DEBUG_LOG please rename) +#ifndef IMGUI_DEBUG_PRINTF +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#define IMGUI_DEBUG_PRINTF(_FMT,...) printf(_FMT, __VA_ARGS__) +#else +#define IMGUI_DEBUG_PRINTF(_FMT,...) ((void)0) +#endif +#endif + +// Debug Logging for ShowDebugLogWindow(). This is designed for relatively rare events so please don't spam. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +#define IMGUI_DEBUG_LOG(...) ImGui::DebugLog(__VA_ARGS__) +#else +#define IMGUI_DEBUG_LOG(...) ((void)0) +#endif +#define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_NAV(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventNav) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_SELECTION(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_CLIPPER(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_IO(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_INPUTROUTING(...) do{if (g.DebugLogFlags & ImGuiDebugLogFlags_EventInputRouting)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) + +// Static Asserts +#define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") + +// "Paranoid" Debug Asserts are meant to only be enabled during specific debugging/work, otherwise would slow down the code too much. +// We currently don't have many of those so the effect is currently negligible, but onward intent to add more aggressive ones in the code. +//#define IMGUI_DEBUG_PARANOID +#ifdef IMGUI_DEBUG_PARANOID +#define IM_ASSERT_PARANOID(_EXPR) IM_ASSERT(_EXPR) +#else +#define IM_ASSERT_PARANOID(_EXPR) #endif +// Error handling +// Down the line in some frameworks/languages we would like to have a way to redirect those to the programmer and recover from more faults. +#ifndef IM_ASSERT_USER_ERROR +#define IM_ASSERT_USER_ERROR(_EXP,_MSG) IM_ASSERT((_EXP) && _MSG) // Recoverable User Error +#endif + +// Misc Macros +#define IM_PI 3.14159265358979323846f +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" // Play it nice with Windows users (Update: since 2018-05, Notepad finally appears to support Unix-style carriage returns!) +#else +#define IM_NEWLINE "\n" +#endif +#ifndef IM_TABSIZE // Until we move this to runtime and/or add proper tab support, at least allow users to compile-time override +#define IM_TABSIZE (4) +#endif +#define IM_MEMALIGN(_OFF,_ALIGN) (((_OFF) + ((_ALIGN) - 1)) & ~((_ALIGN) - 1)) // Memory align e.g. IM_ALIGN(0,4)=0, IM_ALIGN(1,4)=4, IM_ALIGN(4,4)=4, IM_ALIGN(5,4)=8 +#define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose +#define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 +#define IM_TRUNC(_VAL) ((float)(int)(_VAL)) // ImTrunc() is not inlined in MSVC debug builds +#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // +#define IM_STRINGIFY_HELPER(_X) #_X +#define IM_STRINGIFY(_X) IM_STRINGIFY_HELPER(_X) // Preprocessor idiom to stringify e.g. an integer. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +#define IM_FLOOR IM_TRUNC +#endif + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif + +// Warnings +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_MSVC_WARNING_SUPPRESS(XXXX) __pragma(warning(suppress: XXXX)) +#else +#define IM_MSVC_WARNING_SUPPRESS(XXXX) +#endif + +// Debug Tools +// Use 'Metrics/Debugger->Tools->Item Picker' to break into the call-stack of a specific item. +// This will call IM_DEBUG_BREAK() which you may redefine yourself. See https://github.com/scottt/debugbreak for more reference. +#ifndef IM_DEBUG_BREAK +#if defined (_MSC_VER) +#define IM_DEBUG_BREAK() __debugbreak() +#elif defined(__clang__) +#define IM_DEBUG_BREAK() __builtin_debugtrap() +#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define IM_DEBUG_BREAK() __asm__ volatile("int3;nop") +#elif defined(__GNUC__) && defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") +#elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0") +#else +#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +#endif +#endif // #ifndef IM_DEBUG_BREAK + +// Format specifiers, printing 64-bit hasn't been decently standardized... +// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_PRId64 "I64d" +#define IM_PRIu64 "I64u" +#define IM_PRIX64 "I64X" +#else +#define IM_PRId64 "lld" +#define IM_PRIu64 "llu" +#define IM_PRIX64 "llX" +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Generic helpers +// Note that the ImXXX helpers functions are lower-level than ImGui functions. +// ImGui functions or the ImGui context are never called/used from other ImXXX functions. //----------------------------------------------------------------------------- -// Helpers +// - Helpers: Hashing +// - Helpers: Sorting +// - Helpers: Bit manipulation +// - Helpers: String +// - Helpers: Formatting +// - Helpers: UTF-8 <> wchar conversions +// - Helpers: ImVec2/ImVec4 operators +// - Helpers: Maths +// - Helpers: Geometry +// - Helper: ImVec1 +// - Helper: ImVec2ih +// - Helper: ImRect +// - Helper: ImBitArray +// - Helper: ImBitVector +// - Helper: ImSpan<>, ImSpanAllocator<> +// - Helper: ImPool<> +// - Helper: ImChunkStream<> +// - Helper: ImGuiTextIndex +// - Helper: ImGuiStorage //----------------------------------------------------------------------------- -#define IM_PI 3.14159265358979323846f +// Helpers: Hashing +IMGUI_API ImGuiID ImHashData(const void* data, size_t data_size, ImGuiID seed = 0); +IMGUI_API ImGuiID ImHashStr(const char* data, size_t data_size = 0, ImGuiID seed = 0); -// Helpers: UTF-8 <> wchar -IMGUI_API int ImTextStrToUtf8(char *buf, int buf_size, const ImWchar *in_text, const ImWchar *in_text_end); // return output UTF-8 bytes count -IMGUI_API int ImTextCharFromUtf8(unsigned int *out_char, const char *in_text, const char *in_text_end); // return input UTF-8 bytes count -IMGUI_API int ImTextStrFromUtf8(ImWchar *buf, int buf_size, const char *in_text, const char *in_text_end, const char **in_remaining = NULL); // return input UTF-8 bytes count -IMGUI_API int ImTextCountCharsFromUtf8(const char *in_text, const char *in_text_end); // return number of UTF-8 code-points (NOT bytes count) -IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar *in_text, const ImWchar *in_text_end); // return number of bytes to express string as UTF-8 code-points +// Helpers: Sorting +#ifndef ImQsort +static inline void ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); } +#endif -// Helpers: Misc -IMGUI_API ImU32 ImHash(const void *data, int data_size, ImU32 seed = 0); // Pass data_size==0 for zero-terminated strings -IMGUI_API void *ImFileLoadToMemory(const char *filename, const char *file_open_mode, int *out_file_size = NULL, int padding_bytes = 0); -IMGUI_API FILE *ImFileOpen(const char *filename, const char *file_open_mode); -static inline bool ImCharIsSpace(int c) -{ - return c == ' ' || c == '\t' || c == 0x3000; -} -static inline bool ImIsPowerOfTwo(int v) -{ - return v != 0 && (v & (v - 1)) == 0; -} -static inline int ImUpperPowerOfTwo(int v) -{ - v--; - v |= v >> 1; - v |= v >> 2; - v |= v >> 4; - v |= v >> 8; - v |= v >> 16; - v++; - return v; -} +// Helpers: Color Blending +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b); -// Helpers: Geometry -IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &p); -IMGUI_API bool ImTriangleContainsPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p); -IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p); -IMGUI_API void ImTriangleBarycentricCoords(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &p, float &out_u, float &out_v, float &out_w); +// Helpers: Bit manipulation +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } // Helpers: String -IMGUI_API int ImStricmp(const char *str1, const char *str2); -IMGUI_API int ImStrnicmp(const char *str1, const char *str2, size_t count); -IMGUI_API void ImStrncpy(char *dst, const char *src, size_t count); -IMGUI_API char *ImStrdup(const char *str); -IMGUI_API char *ImStrchrRange(const char *str_begin, const char *str_end, char c); -IMGUI_API int ImStrlenW(const ImWchar *str); -IMGUI_API const ImWchar *ImStrbolW(const ImWchar *buf_mid_line, const ImWchar *buf_begin); // Find beginning-of-line -IMGUI_API const char *ImStristr(const char *haystack, const char *haystack_end, const char *needle, const char *needle_end); -IMGUI_API int ImFormatString(char *buf, size_t buf_size, const char *fmt, ...) IM_FMTARGS(3); -IMGUI_API int ImFormatStringV(char *buf, size_t buf_size, const char *fmt, va_list args) IM_FMTLIST(3); - -// Helpers: Math -// We are keeping those not leaking to the user by default, in the case the user has implicit cast operators between ImVec2 and its own types (when IM_VEC2_CLASS_EXTRA is defined) -#ifdef IMGUI_DEFINE_MATH_OPERATORS -static inline ImVec2 operator*(const ImVec2 &lhs, const float rhs) -{ - return ImVec2(lhs.x * rhs, lhs.y * rhs); -} -static inline ImVec2 operator/(const ImVec2 &lhs, const float rhs) +IMGUI_API int ImStricmp(const char* str1, const char* str2); // Case insensitive compare. +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); // Case insensitive compare to a certain count. +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); // Copy to a certain count and always zero terminate (strncpy doesn't). +IMGUI_API char* ImStrdup(const char* str); // Duplicate a string. +IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); // Copy in provided buffer, recreate buffer if needed. +IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); // Find first occurrence of 'c' in string range. +IMGUI_API const char* ImStreolRange(const char* str, const char* str_end); // End end-of-line +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); // Find a substring in a string range. +IMGUI_API void ImStrTrimBlanks(char* str); // Remove leading and trailing blanks from a buffer. +IMGUI_API const char* ImStrSkipBlank(const char* str); // Find first non-blank character. +IMGUI_API int ImStrlenW(const ImWchar* str); // Computer string length (ImWchar string) +IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line (ImWchar string) +IM_MSVC_RUNTIME_CHECKS_OFF +static inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } +static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } +static inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } +static inline bool ImCharIsXdigitA(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Formatting +IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API const char* ImParseFormatFindStart(const char* format); +IMGUI_API const char* ImParseFormatFindEnd(const char* format); +IMGUI_API const char* ImParseFormatTrimDecorations(const char* format, char* buf, size_t buf_size); +IMGUI_API void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size); +IMGUI_API const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size); +IMGUI_API int ImParseFormatPrecision(const char* format, int default_value); + +// Helpers: UTF-8 <> wchar conversions +IMGUI_API const char* ImTextCharToUtf8(char out_buf[5], unsigned int c); // return out_buf +IMGUI_API int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count +IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // read one character. return input UTF-8 bytes count +IMGUI_API int ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count +IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) +IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 +IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 +IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. +IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. + +// Helpers: File System +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef void* ImFileHandle; +static inline ImFileHandle ImFileOpen(const char*, const char*) { return NULL; } +static inline bool ImFileClose(ImFileHandle) { return false; } +static inline ImU64 ImFileGetSize(ImFileHandle) { return (ImU64)-1; } +static inline ImU64 ImFileRead(void*, ImU64, ImU64, ImFileHandle) { return 0; } +static inline ImU64 ImFileWrite(const void*, ImU64, ImU64, ImFileHandle) { return 0; } +#endif +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef FILE* ImFileHandle; +IMGUI_API ImFileHandle ImFileOpen(const char* filename, const char* mode); +IMGUI_API bool ImFileClose(ImFileHandle file); +IMGUI_API ImU64 ImFileGetSize(ImFileHandle file); +IMGUI_API ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file); +IMGUI_API ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file); +#else +#define IMGUI_DISABLE_TTY_FUNCTIONS // Can't use stdout, fflush if we are not using default file functions +#endif +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size = NULL, int padding_bytes = 0); + +// Helpers: Maths +IM_MSVC_RUNTIME_CHECKS_OFF +// - Wrapper for standard libs functions. (Note that imgui_demo.cpp does _not_ use them to keep the code easy to copy) +#ifndef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#define ImFabs(X) fabsf(X) +#define ImSqrt(X) sqrtf(X) +#define ImFmod(X, Y) fmodf((X), (Y)) +#define ImCos(X) cosf(X) +#define ImSin(X) sinf(X) +#define ImAcos(X) acosf(X) +#define ImAtan2(Y, X) atan2f((Y), (X)) +#define ImAtof(STR) atof(STR) +#define ImCeil(X) ceilf(X) +static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision +static inline double ImPow(double x, double y) { return pow(x, y); } +static inline float ImLog(float x) { return logf(x); } // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision +static inline double ImLog(double x) { return log(x); } +static inline int ImAbs(int x) { return x < 0 ? -x : x; } +static inline float ImAbs(float x) { return fabsf(x); } +static inline double ImAbs(double x) { return fabs(x); } +static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument +static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; } +#ifdef IMGUI_ENABLE_SSE +static inline float ImRsqrt(float x) { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); } +#else +static inline float ImRsqrt(float x) { return 1.0f / sqrtf(x); } +#endif +static inline double ImRsqrt(double x) { return 1.0 / sqrt(x); } +#endif +// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double +// (Exceptionally using templates here but we could also redefine them for those types) +template static inline T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; } +template static inline T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; } +template static inline T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } +template static inline T ImLerp(T a, T b, float t) { return (T)(a + (b - a) * t); } +template static inline void ImSwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } +template static inline T ImAddClampOverflow(T a, T b, T mn, T mx) { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; } +template static inline T ImSubClampOverflow(T a, T b, T mn, T mx) { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; } +// - Misc maths helpers +static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2&mn, const ImVec2&mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); } +static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); } +static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } +static inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } +static inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } +static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } +static inline float ImTrunc(float f) { return (float)(int)(f); } +static inline ImVec2 ImTrunc(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } +static inline float ImFloor(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); } +static inline int ImModPositive(int a, int b) { return (a + b) % b; } +static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } +static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } +static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline float ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; } +static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } +static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Geometry +IMGUI_API ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t); +IMGUI_API ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments); // For curves with explicit number of segments +IMGUI_API ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol);// For auto-tessellated curves you can use tess_tol = style.CurveTessellationTol +IMGUI_API ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t); +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); +inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +inline bool ImTriangleIsClockwise(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ((b.x - a.x) * (c.y - b.y)) - ((c.x - b.x) * (b.y - a.y)) > 0.0f; } + +// Helper: ImVec1 (1D vector) +// (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec1 +{ + float x; + constexpr ImVec1() : x(0.0f) { } + constexpr ImVec1(float _x) : x(_x) { } +}; + +// Helper: ImVec2ih (2D vector, half-size integer, for long-term packed storage) +struct ImVec2ih { - return ImVec2(lhs.x / rhs, lhs.y / rhs); -} -static inline ImVec2 operator+(const ImVec2 &lhs, const ImVec2 &rhs) + short x, y; + constexpr ImVec2ih() : x(0), y(0) {} + constexpr ImVec2ih(short _x, short _y) : x(_x), y(_y) {} + constexpr explicit ImVec2ih(const ImVec2& rhs) : x((short)rhs.x), y((short)rhs.y) {} +}; + +// Helper: ImRect (2D axis aligned bounding-box) +// NB: we can't rely on ImVec2 math operators being available here! +struct IMGUI_API ImRect { - return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); + ImVec2 Min; // Upper-left + ImVec2 Max; // Lower-right + + constexpr ImRect() : Min(0.0f, 0.0f), Max(0.0f, 0.0f) {} + constexpr ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} + constexpr ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} + constexpr ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} + + ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } + ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } + float GetWidth() const { return Max.x - Min.x; } + float GetHeight() const { return Max.y - Min.y; } + float GetArea() const { return (Max.x - Min.x) * (Max.y - Min.y); } + ImVec2 GetTL() const { return Min; } // Top-left + ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right + ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left + ImVec2 GetBR() const { return Max; } // Bottom-right + bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } + bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool ContainsWithPad(const ImVec2& p, const ImVec2& pad) const { return p.x >= Min.x - pad.x && p.y >= Min.y - pad.y && p.x < Max.x + pad.x && p.y < Max.y + pad.y; } + bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } + void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } + void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } + void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } + void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } + void TranslateX(float dx) { Min.x += dx; Max.x += dx; } + void TranslateY(float dy) { Min.y += dy; Max.y += dy; } + void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. + void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. + void Floor() { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); } + bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } + ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } +}; + +// Helper: ImBitArray +#define IM_BITARRAY_TESTBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] & ((ImU32)1 << ((_N) & 31))) != 0) // Macro version of ImBitArrayTestBit(): ensure args have side-effect or are costly! +#define IM_BITARRAY_CLEARBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] &= ~((ImU32)1 << ((_N) & 31)))) // Macro version of ImBitArrayClearBit(): ensure args have side-effect or are costly! +inline size_t ImBitArrayGetStorageSizeInBytes(int bitcount) { return (size_t)((bitcount + 31) >> 5) << 2; } +inline void ImBitArrayClearAllBits(ImU32* arr, int bitcount){ memset(arr, 0, ImBitArrayGetStorageSizeInBytes(bitcount)); } +inline bool ImBitArrayTestBit(const ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); return (arr[n >> 5] & mask) != 0; } +inline void ImBitArrayClearBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] &= ~mask; } +inline void ImBitArraySetBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] |= mask; } +inline void ImBitArraySetBitRange(ImU32* arr, int n, int n2) // Works on range [n..n2) +{ + n2--; + while (n <= n2) + { + int a_mod = (n & 31); + int b_mod = (n2 > (n | 31) ? 31 : (n2 & 31)) + 1; + ImU32 mask = (ImU32)(((ImU64)1 << b_mod) - 1) & ~(ImU32)(((ImU64)1 << a_mod) - 1); + arr[n >> 5] |= mask; + n = (n + 32) & ~31; + } } -static inline ImVec2 operator-(const ImVec2 &lhs, const ImVec2 &rhs) + +typedef ImU32* ImBitArrayPtr; // Name for use in structs + +// Helper: ImBitArray class (wrapper over ImBitArray functions) +// Store 1-bit per value. +template +struct ImBitArray +{ + ImU32 Storage[(BITCOUNT + 31) >> 5]; + ImBitArray() { ClearAllBits(); } + void ClearAllBits() { memset(Storage, 0, sizeof(Storage)); } + void SetAllBits() { memset(Storage, 255, sizeof(Storage)); } + bool TestBit(int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } + void SetBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArraySetBit(Storage, n); } + void ClearBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArrayClearBit(Storage, n); } + void SetBitRange(int n, int n2) { n += OFFSET; n2 += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT && n2 > n && n2 <= BITCOUNT); ImBitArraySetBitRange(Storage, n, n2); } // Works on range [n..n2) + bool operator[](int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } +}; + +// Helper: ImBitVector +// Store 1-bit per value. +struct IMGUI_API ImBitVector { - return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); -} -static inline ImVec2 operator*(const ImVec2 &lhs, const ImVec2 &rhs) + ImVector Storage; + void Create(int sz) { Storage.resize((sz + 31) >> 5); memset(Storage.Data, 0, (size_t)Storage.Size * sizeof(Storage.Data[0])); } + void Clear() { Storage.clear(); } + bool TestBit(int n) const { IM_ASSERT(n < (Storage.Size << 5)); return IM_BITARRAY_TESTBIT(Storage.Data, n); } + void SetBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArraySetBit(Storage.Data, n); } + void ClearBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArrayClearBit(Storage.Data, n); } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helper: ImSpan<> +// Pointing to a span of data we don't own. +template +struct ImSpan +{ + T* Data; + T* DataEnd; + + // Constructors, destructor + inline ImSpan() { Data = DataEnd = NULL; } + inline ImSpan(T* data, int size) { Data = data; DataEnd = data + size; } + inline ImSpan(T* data, T* data_end) { Data = data; DataEnd = data_end; } + + inline void set(T* data, int size) { Data = data; DataEnd = data + size; } + inline void set(T* data, T* data_end) { Data = data; DataEnd = data_end; } + inline int size() const { return (int)(ptrdiff_t)(DataEnd - Data); } + inline int size_in_bytes() const { return (int)(ptrdiff_t)(DataEnd - Data) * (int)sizeof(T); } + inline T& operator[](int i) { T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + inline const T& operator[](int i) const { const T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return DataEnd; } + inline const T* end() const { return DataEnd; } + + // Utilities + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < DataEnd); const ptrdiff_t off = it - Data; return (int)off; } +}; + +// Helper: ImSpanAllocator<> +// Facilitate storing multiple chunks into a single large block (the "arena") +// - Usage: call Reserve() N times, allocate GetArenaSizeInBytes() worth, pass it to SetArenaBasePtr(), call GetSpan() N times to retrieve the aligned ranges. +template +struct ImSpanAllocator +{ + char* BasePtr; + int CurrOff; + int CurrIdx; + int Offsets[CHUNKS]; + int Sizes[CHUNKS]; + + ImSpanAllocator() { memset(this, 0, sizeof(*this)); } + inline void Reserve(int n, size_t sz, int a=4) { IM_ASSERT(n == CurrIdx && n < CHUNKS); CurrOff = IM_MEMALIGN(CurrOff, a); Offsets[n] = CurrOff; Sizes[n] = (int)sz; CurrIdx++; CurrOff += (int)sz; } + inline int GetArenaSizeInBytes() { return CurrOff; } + inline void SetArenaBasePtr(void* base_ptr) { BasePtr = (char*)base_ptr; } + inline void* GetSpanPtrBegin(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n]); } + inline void* GetSpanPtrEnd(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n] + Sizes[n]); } + template + inline void GetSpan(int n, ImSpan* span) { span->set((T*)GetSpanPtrBegin(n), (T*)GetSpanPtrEnd(n)); } +}; + +// Helper: ImPool<> +// Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer, +// Honor constructor/destructor. Add/remove invalidate all pointers. Indexes have the same lifetime as the associated object. +typedef int ImPoolIdx; +template +struct ImPool +{ + ImVector Buf; // Contiguous data + ImGuiStorage Map; // ID->Index + ImPoolIdx FreeIdx; // Next free idx to use + ImPoolIdx AliveCount; // Number of active/alive items (for display purpose) + + ImPool() { FreeIdx = AliveCount = 0; } + ~ImPool() { Clear(); } + T* GetByKey(ImGuiID key) { int idx = Map.GetInt(key, -1); return (idx != -1) ? &Buf[idx] : NULL; } + T* GetByIndex(ImPoolIdx n) { return &Buf[n]; } + ImPoolIdx GetIndex(const T* p) const { IM_ASSERT(p >= Buf.Data && p < Buf.Data + Buf.Size); return (ImPoolIdx)(p - Buf.Data); } + T* GetOrAddByKey(ImGuiID key) { int* p_idx = Map.GetIntRef(key, -1); if (*p_idx != -1) return &Buf[*p_idx]; *p_idx = FreeIdx; return Add(); } + bool Contains(const T* p) const { return (p >= Buf.Data && p < Buf.Data + Buf.Size); } + void Clear() { for (int n = 0; n < Map.Data.Size; n++) { int idx = Map.Data[n].val_i; if (idx != -1) Buf[idx].~T(); } Map.Clear(); Buf.clear(); FreeIdx = AliveCount = 0; } + T* Add() { int idx = FreeIdx; if (idx == Buf.Size) { Buf.resize(Buf.Size + 1); FreeIdx++; } else { FreeIdx = *(int*)&Buf[idx]; } IM_PLACEMENT_NEW(&Buf[idx]) T(); AliveCount++; return &Buf[idx]; } + void Remove(ImGuiID key, const T* p) { Remove(key, GetIndex(p)); } + void Remove(ImGuiID key, ImPoolIdx idx) { Buf[idx].~T(); *(int*)&Buf[idx] = FreeIdx; FreeIdx = idx; Map.SetInt(key, -1); AliveCount--; } + void Reserve(int capacity) { Buf.reserve(capacity); Map.Data.reserve(capacity); } + + // To iterate a ImPool: for (int n = 0; n < pool.GetMapSize(); n++) if (T* t = pool.TryGetMapData(n)) { ... } + // Can be avoided if you know .Remove() has never been called on the pool, or AliveCount == GetMapSize() + int GetAliveCount() const { return AliveCount; } // Number of active/alive items in the pool (for display purpose) + int GetBufSize() const { return Buf.Size; } + int GetMapSize() const { return Map.Data.Size; } // It is the map we need iterate to find valid items, since we don't have "alive" storage anywhere + T* TryGetMapData(ImPoolIdx n) { int idx = Map.Data[n].val_i; if (idx == -1) return NULL; return GetByIndex(idx); } +}; + +// Helper: ImChunkStream<> +// Build and iterate a contiguous stream of variable-sized structures. +// This is used by Settings to store persistent data while reducing allocation count. +// We store the chunk size first, and align the final size on 4 bytes boundaries. +// The tedious/zealous amount of casting is to avoid -Wcast-align warnings. +template +struct ImChunkStream +{ + ImVector Buf; + + void clear() { Buf.clear(); } + bool empty() const { return Buf.Size == 0; } + int size() const { return Buf.Size; } + T* alloc_chunk(size_t sz) { size_t HDR_SZ = 4; sz = IM_MEMALIGN(HDR_SZ + sz, 4u); int off = Buf.Size; Buf.resize(off + (int)sz); ((int*)(void*)(Buf.Data + off))[0] = (int)sz; return (T*)(void*)(Buf.Data + off + (int)HDR_SZ); } + T* begin() { size_t HDR_SZ = 4; if (!Buf.Data) return NULL; return (T*)(void*)(Buf.Data + HDR_SZ); } + T* next_chunk(T* p) { size_t HDR_SZ = 4; IM_ASSERT(p >= begin() && p < end()); p = (T*)(void*)((char*)(void*)p + chunk_size(p)); if (p == (T*)(void*)((char*)end() + HDR_SZ)) return (T*)0; IM_ASSERT(p < end()); return p; } + int chunk_size(const T* p) { return ((const int*)p)[-1]; } + T* end() { return (T*)(void*)(Buf.Data + Buf.Size); } + int offset_from_ptr(const T* p) { IM_ASSERT(p >= begin() && p < end()); const ptrdiff_t off = (const char*)p - Buf.Data; return (int)off; } + T* ptr_from_offset(int off) { IM_ASSERT(off >= 4 && off < Buf.Size); return (T*)(void*)(Buf.Data + off); } + void swap(ImChunkStream& rhs) { rhs.Buf.swap(Buf); } +}; + +// Helper: ImGuiTextIndex +// Maintain a line index for a text buffer. This is a strong candidate to be moved into the public API. +struct ImGuiTextIndex { - return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); -} -static inline ImVec2 operator/(const ImVec2 &lhs, const ImVec2 &rhs) + ImVector LineOffsets; + int EndOffset = 0; // Because we don't own text buffer we need to maintain EndOffset (may bake in LineOffsets?) + + void clear() { LineOffsets.clear(); EndOffset = 0; } + int size() { return LineOffsets.Size; } + const char* get_line_begin(const char* base, int n) { return base + LineOffsets[n]; } + const char* get_line_end(const char* base, int n) { return base + (n + 1 < LineOffsets.Size ? (LineOffsets[n + 1] - 1) : EndOffset); } + void append(const char* base, int old_size, int new_size); +}; + +// Helper: ImGuiStorage +IMGUI_API ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key); +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList support +//----------------------------------------------------------------------------- + +// ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value. +// Estimation of number of circle segment based on error is derived using method described in https://stackoverflow.com/a/2244088/15194693 +// Number of segments (N) is calculated using equation: +// N = ceil ( pi / acos(1 - error / r) ) where r > 0, error <= r +// Our equation is significantly simpler that one in the post thanks for choosing segment that is +// perpendicular to X axis. Follow steps in the article from this starting condition and you will +// will get this result. +// +// Rendering circles with an odd number of segments, while mathematically correct will produce +// asymmetrical results on the raster grid. Therefore we're rounding N to next even number (7->8, 8->8, 9->10 etc.) +#define IM_ROUNDUP_TO_EVEN(_V) ((((_V) + 1) / 2) * 2) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 4 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp(IM_ROUNDUP_TO_EVEN((int)ImCeil(IM_PI / ImAcos(1 - ImMin((_MAXERROR), (_RAD)) / (_RAD)))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) + +// Raw equation from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC rewritten for 'r' and 'error'. +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(_N,_MAXERROR) ((_MAXERROR) / (1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI)))) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_ERROR(_N,_RAD) ((1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI))) / (_RAD)) + +// ImDrawList: Lookup table size for adaptive arc drawing, cover full circle. +#ifndef IM_DRAWLIST_ARCFAST_TABLE_SIZE +#define IM_DRAWLIST_ARCFAST_TABLE_SIZE 48 // Number of samples in lookup table. +#endif +#define IM_DRAWLIST_ARCFAST_SAMPLE_MAX IM_DRAWLIST_ARCFAST_TABLE_SIZE // Sample index _PathArcToFastEx() for 360 angle. + +// Data shared between all ImDrawList instances +// You may want to create your own instance of this if you want to use ImDrawList completely without ImGui. In that case, watch out for future changes to this structure. +struct IMGUI_API ImDrawListSharedData { - return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); -} -static inline ImVec2 &operator+=(ImVec2 &lhs, const ImVec2 &rhs) + ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas + ImFont* Font; // Current/default font (optional, for simplified AddText overload) + float FontSize; // Current/default font size (optional, for simplified AddText overload) + float FontScale; // Current/default font scale (== FontSize / Font->FontSize) + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() + float CircleSegmentMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc + ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) + + // [Internal] Temp write buffer + ImVector TempBuffer; + + // [Internal] Lookup tables + ImVec2 ArcFastVtx[IM_DRAWLIST_ARCFAST_TABLE_SIZE]; // Sample points on the quarter of the circle. + float ArcFastRadiusCutoff; // Cutoff radius after which arc drawing will fallback to slower PathArcTo() + ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas + + ImDrawListSharedData(); + void SetCircleTessellationMaxError(float max_error); +}; + +struct ImDrawDataBuilder { - lhs.x += rhs.x; - lhs.y += rhs.y; - return lhs; -} -static inline ImVec2 &operator-=(ImVec2 &lhs, const ImVec2 &rhs) + ImVector* Layers[2]; // Pointers to global layers for: regular, tooltip. LayersP[0] is owned by DrawData. + ImVector LayerData1; + + ImDrawDataBuilder() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Data types support +//----------------------------------------------------------------------------- + +struct ImGuiDataVarInfo { - lhs.x -= rhs.x; - lhs.y -= rhs.y; - return lhs; -} -static inline ImVec2 &operator*=(ImVec2 &lhs, const float rhs) + ImGuiDataType Type; + ImU32 Count; // 1+ + ImU32 Offset; // Offset in parent structure + void* GetVarPtr(void* parent) const { return (void*)((unsigned char*)parent + Offset); } +}; + +struct ImGuiDataTypeStorage { - lhs.x *= rhs; - lhs.y *= rhs; - return lhs; -} -static inline ImVec2 &operator/=(ImVec2 &lhs, const float rhs) + ImU8 Data[8]; // Opaque storage to fit any data up to ImGuiDataType_COUNT +}; + +// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). +struct ImGuiDataTypeInfo { - lhs.x /= rhs; - lhs.y /= rhs; - return lhs; -} -static inline ImVec4 operator+(const ImVec4 &lhs, const ImVec4 &rhs) + size_t Size; // Size in bytes + const char* Name; // Short descriptive name for the type, for debugging + const char* PrintFmt; // Default printf format for the type + const char* ScanFmt; // Default scanf format for the type +}; + +// Extend ImGuiDataType_ +enum ImGuiDataTypePrivate_ { - return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); -} -static inline ImVec4 operator-(const ImVec4 &lhs, const ImVec4 &rhs) + ImGuiDataType_String = ImGuiDataType_COUNT + 1, + ImGuiDataType_Pointer, + ImGuiDataType_ID, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Widgets support: flags, enums, data structures +//----------------------------------------------------------------------------- + +// Extend ImGuiItemFlags +// - input: PushItemFlag() manipulates g.CurrentItemFlags, ItemAdd() calls may add extra flags. +// - output: stored in g.LastItemData.InFlags +enum ImGuiItemFlagsPrivate_ { - return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); -} -static inline ImVec4 operator*(const ImVec4 &lhs, const ImVec4 &rhs) + // Controlled by user + ImGuiItemFlags_Disabled = 1 << 10, // false // Disable interactions (DOES NOT affect visuals, see BeginDisabled()/EndDisabled() for full disable feature, and github #211). + ImGuiItemFlags_ReadOnly = 1 << 11, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. + ImGuiItemFlags_MixedValue = 1 << 12, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) + ImGuiItemFlags_NoWindowHoverableCheck = 1 << 13, // false // Disable hoverable check in ItemHoverable() + ImGuiItemFlags_AllowOverlap = 1 << 14, // false // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame. + + // Controlled by widget code + ImGuiItemFlags_Inputable = 1 << 20, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. + ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData() + ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData() + + ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead. + + // Obsolete + //ImGuiItemFlags_SelectableDontClosePopup = !ImGuiItemFlags_AutoClosePopups, // Can't have a redirect as we inverted the behavior +}; + +// Status flags for an already submitted item +// - output: stored in g.LastItemData.StatusFlags +enum ImGuiItemStatusFlags_ { - return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); -} + ImGuiItemStatusFlags_None = 0, + ImGuiItemStatusFlags_HoveredRect = 1 << 0, // Mouse position is within item rectangle (does NOT mean that the window is in correct z-order and can be hovered!, this is only one part of the most-common IsItemHovered test) + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, // g.LastItemData.DisplayRect is valid + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3, // Set when Selectable(), TreeNode() reports toggling a selection. We can't report "Selected", only state changes, in order to easily handle clipping with less issues. + ImGuiItemStatusFlags_ToggledOpen = 1 << 4, // Set when TreeNode() reports toggling their open state. + ImGuiItemStatusFlags_HasDeactivated = 1 << 5, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. + ImGuiItemStatusFlags_Deactivated = 1 << 6, // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. + ImGuiItemStatusFlags_HoveredWindow = 1 << 7, // Override the HoveredWindow test to allow cross-window hover testing. + ImGuiItemStatusFlags_Visible = 1 << 8, // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()). + ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid. + ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd(). + + // Additional status + semantic for ImGuiTestEngine +#ifdef IMGUI_ENABLE_TEST_ENGINE + ImGuiItemStatusFlags_Openable = 1 << 20, // Item is an openable (e.g. TreeNode) + ImGuiItemStatusFlags_Opened = 1 << 21, // Opened status + ImGuiItemStatusFlags_Checkable = 1 << 22, // Item is a checkable (e.g. CheckBox, MenuItem) + ImGuiItemStatusFlags_Checked = 1 << 23, // Checked status + ImGuiItemStatusFlags_Inputable = 1 << 24, // Item is a text-inputable (e.g. InputText, SliderXXX, DragXXX) #endif +}; -static inline int ImMin(int lhs, int rhs) +// Extend ImGuiHoveredFlags_ +enum ImGuiHoveredFlagsPrivate_ { - return lhs < rhs ? lhs : rhs; -} -static inline int ImMax(int lhs, int rhs) + ImGuiHoveredFlags_DelayMask_ = ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, + ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, + ImGuiHoveredFlags_AllowedMaskForIsItemHovered = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayMask_, +}; + +// Extend ImGuiInputTextFlags_ +enum ImGuiInputTextFlagsPrivate_ { - return lhs >= rhs ? lhs : rhs; -} -static inline float ImMin(float lhs, float rhs) + // [Internal] + ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() + ImGuiInputTextFlags_NoMarkEdited = 1 << 27, // For internal use by functions using InputText() before reformatting data + ImGuiInputTextFlags_MergedItem = 1 << 28, // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. + ImGuiInputTextFlags_LocalizeDecimalPoint= 1 << 29, // For internal use by InputScalar() and TempInputScalar() +}; + +// Extend ImGuiButtonFlags_ +enum ImGuiButtonFlagsPrivate_ +{ + ImGuiButtonFlags_PressedOnClick = 1 << 4, // return true on click (mouse down event) + ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, // [Default] return true on click + release on same item <-- this is what the majority of Button are using + ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, // return true on click + release even if the release event is not done while hovering the item + ImGuiButtonFlags_PressedOnRelease = 1 << 7, // return true on release (default requires click+release) + ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, // return true on double-click (default requires click+release) + ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, // return true when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) + ImGuiButtonFlags_Repeat = 1 << 10, // hold to repeat + ImGuiButtonFlags_FlattenChildren = 1 << 11, // allow interactions even if a child window is overlapping + ImGuiButtonFlags_AllowOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable. + ImGuiButtonFlags_DontClosePopups = 1 << 13, // disable automatically closing parent popup on press // [UNUSED] + //ImGuiButtonFlags_Disabled = 1 << 14, // disable interactions -> use BeginDisabled() or ImGuiItemFlags_Disabled + ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine + ImGuiButtonFlags_NoKeyModifiers = 1 << 16, // disable mouse interaction if a key modifier is held + ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) + ImGuiButtonFlags_NoNavFocus = 1 << 18, // don't override navigation focus when activated (FIXME: this is essentially used every time an item uses ImGuiItemFlags_NoNav, but because legacy specs don't requires LastItemData to be set ButtonBehavior(), we can't poll g.LastItemData.InFlags) + ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, // don't report as hovered when nav focus is on this item + ImGuiButtonFlags_NoSetKeyOwner = 1 << 20, // don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_NoTestKeyOwner = 1 << 21, // don't test key/input owner when polling the key (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_PressedOnMask_ = ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_PressedOnDragDropHold, + ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease, +}; + +// Extend ImGuiComboFlags_ +enum ImGuiComboFlagsPrivate_ { - return lhs < rhs ? lhs : rhs; -} -static inline float ImMax(float lhs, float rhs) + ImGuiComboFlags_CustomPreview = 1 << 20, // enable BeginComboPreview() +}; + +// Extend ImGuiSliderFlags_ +enum ImGuiSliderFlagsPrivate_ { - return lhs >= rhs ? lhs : rhs; -} -static inline ImVec2 ImMin(const ImVec2 &lhs, const ImVec2 &rhs) + ImGuiSliderFlags_Vertical = 1 << 20, // Should this slider be orientated vertically? + ImGuiSliderFlags_ReadOnly = 1 << 21, // Consider using g.NextItemData.ItemFlags |= ImGuiItemFlags_ReadOnly instead. +}; + +// Extend ImGuiSelectableFlags_ +enum ImGuiSelectableFlagsPrivate_ { - return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); -} -static inline ImVec2 ImMax(const ImVec2 &lhs, const ImVec2 &rhs) + // NB: need to be in sync with last value of ImGuiSelectableFlags_ + ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, + ImGuiSelectableFlags_SelectOnNav = 1 << 21, // (WIP) Auto-select when moved into. This is not exposed in public API as to handle multi-select and modifiers we will need user to explicitly control focus scope. May be replaced with a BeginSelection() API. + ImGuiSelectableFlags_SelectOnClick = 1 << 22, // Override button behavior to react on Click (default is Click+Release) + ImGuiSelectableFlags_SelectOnRelease = 1 << 23, // Override button behavior to react on Release (default is Click+Release) + ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, // Span all avail width even if we declared less for layout purpose. FIXME: We may be able to remove this (added in 6251d379, 2bcafc86 for menus) + ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25, // Set Nav/Focus ID on mouse hover (used by MenuItem) + ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 26, // Disable padding each side with ItemSpacing * 0.5f + ImGuiSelectableFlags_NoSetKeyOwner = 1 << 27, // Don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) +}; + +// Extend ImGuiTreeNodeFlags_ +enum ImGuiTreeNodeFlagsPrivate_ { - return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); -} -static inline int ImClamp(int v, int mn, int mx) + ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 28,// FIXME-WIP: Hard-coded for CollapsingHeader() + ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 29,// FIXME-WIP: Turn Down arrow into an Up arrow, but reversed trees (#6517) + ImGuiTreeNodeFlags_OpenOnMask_ = ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow, +}; + +enum ImGuiSeparatorFlags_ { - return (v < mn) ? mn : (v > mx) ? mx : - v; -} -static inline float ImClamp(float v, float mn, float mx) + ImGuiSeparatorFlags_None = 0, + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1, + ImGuiSeparatorFlags_SpanAllColumns = 1 << 2, // Make separator cover all columns of a legacy Columns() set. +}; + +// Flags for FocusWindow(). This is not called ImGuiFocusFlags to avoid confusion with public-facing ImGuiFocusedFlags. +// FIXME: Once we finishing replacing more uses of GetTopMostPopupModal()+IsWindowWithinBeginStackOf() +// and FindBlockingModal() with this, we may want to change the flag to be opt-out instead of opt-in. +enum ImGuiFocusRequestFlags_ { - return (v < mn) ? mn : (v > mx) ? mx : - v; -} -static inline ImVec2 ImClamp(const ImVec2 &f, const ImVec2 &mn, ImVec2 mx) + ImGuiFocusRequestFlags_None = 0, + ImGuiFocusRequestFlags_RestoreFocusedChild = 1 << 0, // Find last focused child (if any) and focus it instead. + ImGuiFocusRequestFlags_UnlessBelowModal = 1 << 1, // Do not set focus if the window is below a modal. +}; + +enum ImGuiTextFlags_ { - return ImVec2(ImClamp(f.x, mn.x, mx.x), ImClamp(f.y, mn.y, mx.y)); -} -static inline float ImSaturate(float f) + ImGuiTextFlags_None = 0, + ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0, +}; + +enum ImGuiTooltipFlags_ { - return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : - f; -} -static inline void ImSwap(int &a, int &b) + ImGuiTooltipFlags_None = 0, + ImGuiTooltipFlags_OverridePrevious = 1 << 1, // Clear/ignore previously submitted tooltip (defaults to append) +}; + +// FIXME: this is in development, not exposed/functional as a generic feature yet. +// Horizontal/Vertical enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiLayoutType_ { - int tmp = a; - a = b; - b = tmp; -} -static inline void ImSwap(float &a, float &b) + ImGuiLayoutType_Horizontal = 0, + ImGuiLayoutType_Vertical = 1 +}; + +enum ImGuiLogType { - float tmp = a; - a = b; - b = tmp; -} -static inline int ImLerp(int a, int b, float t) + ImGuiLogType_None = 0, + ImGuiLogType_TTY, + ImGuiLogType_File, + ImGuiLogType_Buffer, + ImGuiLogType_Clipboard, +}; + +// X/Y enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiAxis { - return (int) (a + (b - a) * t); -} -static inline float ImLerp(float a, float b, float t) + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 +}; + +enum ImGuiPlotType { - return a + (b - a) * t; -} -static inline ImVec2 ImLerp(const ImVec2 &a, const ImVec2 &b, float t) + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram, +}; + +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColorMod { - return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); -} -static inline ImVec2 ImLerp(const ImVec2 &a, const ImVec2 &b, const ImVec2 &t) + ImGuiCol Col; + ImVec4 BackupValue; +}; + +// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. +struct ImGuiStyleMod { - return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); -} -static inline ImVec4 ImLerp(const ImVec4 &a, const ImVec4 &b, float t) + ImGuiStyleVar VarIdx; + union { int BackupInt[2]; float BackupFloat[2]; }; + ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } +}; + +// Storage data for BeginComboPreview()/EndComboPreview() +struct IMGUI_API ImGuiComboPreviewData { - return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); -} -static inline float ImLengthSqr(const ImVec2 &lhs) + ImRect PreviewRect; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + float BackupPrevLineTextBaseOffset; + ImGuiLayoutType BackupLayout; + + ImGuiComboPreviewData() { memset(this, 0, sizeof(*this)); } +}; + +// Stacked storage data for BeginGroup()/EndGroup() +struct IMGUI_API ImGuiGroupData +{ + ImGuiID WindowID; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + ImVec1 BackupIndent; + ImVec1 BackupGroupOffset; + ImVec2 BackupCurrLineSize; + float BackupCurrLineTextBaseOffset; + ImGuiID BackupActiveIdIsAlive; + bool BackupActiveIdPreviousFrameIsAlive; + bool BackupHoveredIdIsAlive; + bool BackupIsSameLine; + bool EmitItem; +}; + +// Simple column measurement, currently used for MenuItem() only.. This is very short-sighted/throw-away code and NOT a generic helper. +struct IMGUI_API ImGuiMenuColumns { - return lhs.x * lhs.x + lhs.y * lhs.y; -} -static inline float ImLengthSqr(const ImVec4 &lhs) + ImU32 TotalWidth; + ImU32 NextTotalWidth; + ImU16 Spacing; + ImU16 OffsetIcon; // Always zero for now + ImU16 OffsetLabel; // Offsets are locked in Update() + ImU16 OffsetShortcut; + ImU16 OffsetMark; + ImU16 Widths[4]; // Width of: Icon, Label, Shortcut, Mark (accumulators for current frame) + + ImGuiMenuColumns() { memset(this, 0, sizeof(*this)); } + void Update(float spacing, bool window_reappearing); + float DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark); + void CalcNextTotalWidth(bool update_offsets); +}; + +// Internal temporary state for deactivating InputText() instances. +struct IMGUI_API ImGuiInputTextDeactivatedState { - return lhs.x * lhs.x + lhs.y * lhs.y + lhs.z * lhs.z + lhs.w * lhs.w; -} -static inline float ImInvLength(const ImVec2 &lhs, float fail_value) + ImGuiID ID; // widget id owning the text state (which just got deactivated) + ImVector TextA; // text buffer + + ImGuiInputTextDeactivatedState() { memset(this, 0, sizeof(*this)); } + void ClearFreeMemory() { ID = 0; TextA.clear(); } +}; +// Internal state of the currently focused/edited text input box +// For a given item ID, access with ImGui::GetInputTextState() +struct IMGUI_API ImGuiInputTextState +{ + ImGuiContext* Ctx; // parent UI context (needs to be set explicitly by parent). + ImGuiID ID; // widget id owning the text state + int CurLenW, CurLenA; // we need to maintain our buffer length in both UTF-8 and wchar format. UTF-8 length is valid even if TextA is not. + ImVector TextW; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. + ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. + ImVector InitialTextA; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) + bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) + int BufCapacityA; // end-user buffer capacity + ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) + ImStb::STB_TexteditState Stb; // state for stb_textedit.h + float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately + bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) + bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection + bool Edited; // edited this frame + ImGuiInputTextFlags Flags; // copy of InputText() flags. may be used to check if e.g. ImGuiInputTextFlags_Password is set. + bool ReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. + int ReloadSelectionStart; // POSITIONS ARE IN IMWCHAR units *NOT* UTF-8 this is why this is not exposed yet. + int ReloadSelectionEnd; + + ImGuiInputTextState() { memset(this, 0, sizeof(*this)); } + void ClearText() { CurLenW = CurLenA = 0; TextW[0] = 0; TextA[0] = 0; CursorClamp(); } + void ClearFreeMemory() { TextW.clear(); TextA.clear(); InitialTextA.clear(); } + int GetUndoAvailCount() const { return Stb.undostate.undo_point; } + int GetRedoAvailCount() const { return IMSTB_TEXTEDIT_UNDOSTATECOUNT - Stb.undostate.redo_point; } + void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation + + // Cursor & Selection + void CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking + void CursorClamp() { Stb.cursor = ImMin(Stb.cursor, CurLenW); Stb.select_start = ImMin(Stb.select_start, CurLenW); Stb.select_end = ImMin(Stb.select_end, CurLenW); } + bool HasSelection() const { return Stb.select_start != Stb.select_end; } + void ClearSelection() { Stb.select_start = Stb.select_end = Stb.cursor; } + int GetCursorPos() const { return Stb.cursor; } + int GetSelectionStart() const { return Stb.select_start; } + int GetSelectionEnd() const { return Stb.select_end; } + void SelectAll() { Stb.select_start = 0; Stb.cursor = Stb.select_end = CurLenW; Stb.has_preferred_x = 0; } + + // Reload user buf (WIP #2890) + // If you modify underlying user-passed const char* while active you need to call this (InputText V2 may lift this) + // strcpy(my_buf, "hello"); + // if (ImGuiInputTextState* state = ImGui::GetInputTextState(id)) // id may be ImGui::GetItemID() is last item + // state->ReloadUserBufAndSelectAll(); + void ReloadUserBufAndSelectAll() { ReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } + void ReloadUserBufAndKeepSelection() { ReloadUserBuf = true; ReloadSelectionStart = Stb.select_start; ReloadSelectionEnd = Stb.select_end; } + void ReloadUserBufAndMoveToEnd() { ReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; } + +}; + +enum ImGuiWindowRefreshFlags_ { - float d = lhs.x * lhs.x + lhs.y * lhs.y; - if (d > 0.0f) - return 1.0f / sqrtf(d); - return fail_value; -} -static inline float ImFloor(float f) + ImGuiWindowRefreshFlags_None = 0, + ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, // [EXPERIMENTAL] Try to keep existing contents, USER MUST NOT HONOR BEGIN() RETURNING FALSE AND NOT APPEND. + ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, // [EXPERIMENTAL] Always refresh on hover + ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, // [EXPERIMENTAL] Always refresh on focus + // Refresh policy/frequency, Load Balancing etc. +}; + +enum ImGuiNextWindowDataFlags_ +{ + ImGuiNextWindowDataFlags_None = 0, + ImGuiNextWindowDataFlags_HasPos = 1 << 0, + ImGuiNextWindowDataFlags_HasSize = 1 << 1, + ImGuiNextWindowDataFlags_HasContentSize = 1 << 2, + ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, + ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, + ImGuiNextWindowDataFlags_HasFocus = 1 << 5, + ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, + ImGuiNextWindowDataFlags_HasScroll = 1 << 7, + ImGuiNextWindowDataFlags_HasChildFlags = 1 << 8, + ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 9, +}; + +// Storage for SetNexWindow** functions +struct ImGuiNextWindowData { - return (float) (int) f; -} -static inline ImVec2 ImFloor(const ImVec2 &v) + ImGuiNextWindowDataFlags Flags; + ImGuiCond PosCond; + ImGuiCond SizeCond; + ImGuiCond CollapsedCond; + ImVec2 PosVal; + ImVec2 PosPivotVal; + ImVec2 SizeVal; + ImVec2 ContentSizeVal; + ImVec2 ScrollVal; + ImGuiChildFlags ChildFlags; + bool CollapsedVal; + ImRect SizeConstraintRect; + ImGuiSizeCallback SizeCallback; + void* SizeCallbackUserData; + float BgAlphaVal; // Override background alpha + ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) + ImGuiWindowRefreshFlags RefreshFlagsVal; + + ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } + inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } +}; + +enum ImGuiNextItemDataFlags_ { - return ImVec2((float) (int) v.x, (float) (int) v.y); -} -static inline float ImDot(const ImVec2 &a, const ImVec2 &b) + ImGuiNextItemDataFlags_None = 0, + ImGuiNextItemDataFlags_HasWidth = 1 << 0, + ImGuiNextItemDataFlags_HasOpen = 1 << 1, + ImGuiNextItemDataFlags_HasShortcut = 1 << 2, + ImGuiNextItemDataFlags_HasRefVal = 1 << 3, + ImGuiNextItemDataFlags_HasStorageID = 1 << 4, +}; + +struct ImGuiNextItemData +{ + ImGuiNextItemDataFlags Flags; + ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap and ImGuiItemFlags_HasSelectionUserData. + // Non-flags members are NOT cleared by ItemAdd() meaning they are still valid during NavProcessItem() + ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() + ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) + float Width; // Set by SetNextItemWidth() + ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() + ImGuiInputFlags ShortcutFlags; // Set by SetNextItemShortcut() + bool OpenVal; // Set by SetNextItemOpen() + ImU8 OpenCond; // Set by SetNextItemOpen() + ImGuiDataTypeStorage RefVal; // Not exposed yet, for ImGuiInputTextFlags_ParseEmptyAsRefVal + ImGuiID StorageId; // Set by SetNextItemStorageID() + + ImGuiNextItemData() { memset(this, 0, sizeof(*this)); SelectionUserData = -1; } + inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; ItemFlags = ImGuiItemFlags_None; } // Also cleared manually by ItemAdd()! +}; + +// Status storage for the last submitted item +struct ImGuiLastItemData { - return a.x * b.x + a.y * b.y; -} -static inline ImVec2 ImRotate(const ImVec2 &v, float cos_a, float sin_a) + ImGuiID ID; + ImGuiItemFlags InFlags; // See ImGuiItemFlags_ + ImGuiItemStatusFlags StatusFlags; // See ImGuiItemStatusFlags_ + ImRect Rect; // Full rectangle + ImRect NavRect; // Navigation scoring rectangle (not displayed) + // Rarely used fields are not explicitly cleared, only valid when the corresponding ImGuiItemStatusFlags ar set. + ImRect DisplayRect; // Display rectangle. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) is set. + ImRect ClipRect; // Clip rectangle at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasClipRect) is set.. + ImGuiKeyChord Shortcut; // Shortcut at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasShortcut) is set.. + + ImGuiLastItemData() { memset(this, 0, sizeof(*this)); } +}; + +// Store data emitted by TreeNode() for usage by TreePop() +// - To implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere: store the minimum amount of data +// which we can't infer in TreePop(), to perform the equivalent of NavApplyItemToResult(). +// Only stored when the node is a potential candidate for landing on a Left arrow jump. +struct ImGuiTreeNodeStackData { - return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); -} -static inline float ImLinearSweep(float current, float target, float speed) + ImGuiID ID; + ImGuiTreeNodeFlags TreeFlags; + ImGuiItemFlags InFlags; // Used for nav landing + ImRect NavRect; // Used for nav landing +}; + +struct IMGUI_API ImGuiStackSizes +{ + short SizeOfIDStack; + short SizeOfColorStack; + short SizeOfStyleVarStack; + short SizeOfFontStack; + short SizeOfFocusScopeStack; + short SizeOfGroupStack; + short SizeOfItemFlagsStack; + short SizeOfBeginPopupStack; + short SizeOfDisabledStack; + + ImGuiStackSizes() { memset(this, 0, sizeof(*this)); } + void SetToContextState(ImGuiContext* ctx); + void CompareWithContextState(ImGuiContext* ctx); +}; + +// Data saved for each window pushed into the stack +struct ImGuiWindowStackData { - if (current < target) - return ImMin(current + speed, target); - if (current > target) - return ImMax(current - speed, target); - return current; -} -static inline ImVec2 ImMul(const ImVec2 &lhs, const ImVec2 &rhs) + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiStackSizes StackSizesOnBegin; // Store size of various stacks for asserting + bool DisabledOverrideReenable; // Non-child window override disabled flag +}; + +struct ImGuiShrinkWidthItem { - return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); -} + int Index; + float Width; + float InitialWidth; +}; -// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. -// Defining a custom placement new() with a dummy parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. -struct ImNewPlacementDummy -{}; -inline void *operator new(size_t, ImNewPlacementDummy, void *ptr) +struct ImGuiPtrOrIndex { - return ptr; -} -inline void operator delete(void *, ImNewPlacementDummy, void *) -{} // This is only required so we can use the symetrical new() -#define IM_PLACEMENT_NEW(_PTR) new (ImNewPlacementDummy(), _PTR) -#define IM_NEW(_TYPE) new (ImNewPlacementDummy(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE -template -void IM_DELETE(T *&p) -{ - if (p) - { - p->~T(); - ImGui::MemFree(p); - p = NULL; - } -} + void* Ptr; // Either field can be set, not both. e.g. Dock node tab bars are loose while BeginTabBar() ones are in a pool. + int Index; // Usually index in a main pool. + + ImGuiPtrOrIndex(void* ptr) { Ptr = ptr; Index = -1; } + ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } +}; //----------------------------------------------------------------------------- -// Types +// [SECTION] Popup support //----------------------------------------------------------------------------- -enum ImGuiButtonFlags_ +enum ImGuiPopupPositionPolicy { - ImGuiButtonFlags_Repeat = 1 << 0, // hold to repeat - ImGuiButtonFlags_PressedOnClickRelease = 1 << 1, // return true on click + release on same item [DEFAULT if no PressedOn* flag is set] - ImGuiButtonFlags_PressedOnClick = 1 << 2, // return true on click (default requires click+release) - ImGuiButtonFlags_PressedOnRelease = 1 << 3, // return true on release (default requires click+release) - ImGuiButtonFlags_PressedOnDoubleClick = 1 << 4, // return true on double-click (default requires click+release) - ImGuiButtonFlags_FlattenChildren = 1 << 5, // allow interactions even if a child window is overlapping - ImGuiButtonFlags_AllowItemOverlap = 1 << 6, // require previous frame HoveredId to either match id or be null before being usable, use along with SetItemAllowOverlap() - ImGuiButtonFlags_DontClosePopups = 1 << 7, // disable automatically closing parent popup on press // [UNUSED] - ImGuiButtonFlags_Disabled = 1 << 8, // disable interactions - ImGuiButtonFlags_AlignTextBaseLine = 1 << 9, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine - ImGuiButtonFlags_NoKeyModifiers = 1 << 10, // disable interaction if a key modifier is held - ImGuiButtonFlags_NoHoldingActiveID = 1 << 11, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) - ImGuiButtonFlags_PressedOnDragDropHold = 1 << 12, // press when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) - ImGuiButtonFlags_NoNavFocus = 1 << 13 // don't override navigation focus when activated + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox, + ImGuiPopupPositionPolicy_Tooltip, }; -enum ImGuiSliderFlags_ +// Storage for popup stacks (g.OpenPopupStack and g.BeginPopupStack) +struct ImGuiPopupData { - ImGuiSliderFlags_Vertical = 1 << 0 + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* RestoreNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close + int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + + ImGuiPopupData() { memset(this, 0, sizeof(*this)); ParentNavLayer = OpenFrameCount = -1; } }; -enum ImGuiColumnsFlags_ -{ - // Default: 0 - ImGuiColumnsFlags_NoBorder = 1 << 0, // Disable column dividers - ImGuiColumnsFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers - ImGuiColumnsFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns - ImGuiColumnsFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window - ImGuiColumnsFlags_GrowParentContentsSize = 1 << 4 // (WIP) Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. +//----------------------------------------------------------------------------- +// [SECTION] Inputs support +//----------------------------------------------------------------------------- + +// Bit array for named keys +typedef ImBitArray ImBitArrayForNamedKeys; + +// [Internal] Key ranges +#define ImGuiKey_LegacyNativeKey_BEGIN 0 +#define ImGuiKey_LegacyNativeKey_END 512 +#define ImGuiKey_Keyboard_BEGIN (ImGuiKey_NamedKey_BEGIN) +#define ImGuiKey_Keyboard_END (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_BEGIN (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_END (ImGuiKey_GamepadRStickDown + 1) +#define ImGuiKey_Mouse_BEGIN (ImGuiKey_MouseLeft) +#define ImGuiKey_Mouse_END (ImGuiKey_MouseWheelY + 1) +#define ImGuiKey_Aliases_BEGIN (ImGuiKey_Mouse_BEGIN) +#define ImGuiKey_Aliases_END (ImGuiKey_Mouse_END) + +// [Internal] Named shortcuts for Navigation +#define ImGuiKey_NavKeyboardTweakSlow ImGuiMod_Ctrl +#define ImGuiKey_NavKeyboardTweakFast ImGuiMod_Shift +#define ImGuiKey_NavGamepadTweakSlow ImGuiKey_GamepadL1 +#define ImGuiKey_NavGamepadTweakFast ImGuiKey_GamepadR1 +#define ImGuiKey_NavGamepadActivate (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceRight : ImGuiKey_GamepadFaceDown) +#define ImGuiKey_NavGamepadCancel (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceDown : ImGuiKey_GamepadFaceRight) +#define ImGuiKey_NavGamepadMenu ImGuiKey_GamepadFaceLeft +#define ImGuiKey_NavGamepadInput ImGuiKey_GamepadFaceUp + +enum ImGuiInputEventType +{ + ImGuiInputEventType_None = 0, + ImGuiInputEventType_MousePos, + ImGuiInputEventType_MouseWheel, + ImGuiInputEventType_MouseButton, + ImGuiInputEventType_Key, + ImGuiInputEventType_Text, + ImGuiInputEventType_Focus, + ImGuiInputEventType_COUNT }; -enum ImGuiSelectableFlagsPrivate_ +enum ImGuiInputSource { - // NB: need to be in sync with last value of ImGuiSelectableFlags_ - ImGuiSelectableFlags_Menu = 1 << 3, // -> PressedOnClick - ImGuiSelectableFlags_MenuItem = 1 << 4, // -> PressedOnRelease - ImGuiSelectableFlags_Disabled = 1 << 5, - ImGuiSelectableFlags_DrawFillAvailWidth = 1 << 6 + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, // Note: may be Mouse or TouchScreen or Pen. See io.MouseSource to distinguish them. + ImGuiInputSource_Keyboard, + ImGuiInputSource_Gamepad, + ImGuiInputSource_COUNT }; -enum ImGuiSeparatorFlags_ -{ - ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar - ImGuiSeparatorFlags_Vertical = 1 << 1 +// FIXME: Structures in the union below need to be declared as anonymous unions appears to be an extension? +// Using ImVec2() would fail on Clang 'union member 'MousePos' has a non-trivial default constructor' +struct ImGuiInputEventMousePos { float PosX, PosY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseWheel { float WheelX, WheelY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseButton { int Button; bool Down; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventKey { ImGuiKey Key; bool Down; float AnalogValue; }; +struct ImGuiInputEventText { unsigned int Char; }; +struct ImGuiInputEventAppFocused { bool Focused; }; + +struct ImGuiInputEvent +{ + ImGuiInputEventType Type; + ImGuiInputSource Source; + ImU32 EventId; // Unique, sequential increasing integer to identify an event (if you need to correlate them to other data). + union + { + ImGuiInputEventMousePos MousePos; // if Type == ImGuiInputEventType_MousePos + ImGuiInputEventMouseWheel MouseWheel; // if Type == ImGuiInputEventType_MouseWheel + ImGuiInputEventMouseButton MouseButton; // if Type == ImGuiInputEventType_MouseButton + ImGuiInputEventKey Key; // if Type == ImGuiInputEventType_Key + ImGuiInputEventText Text; // if Type == ImGuiInputEventType_Text + ImGuiInputEventAppFocused AppFocused; // if Type == ImGuiInputEventType_Focus + }; + bool AddedByTestEngine; + + ImGuiInputEvent() { memset(this, 0, sizeof(*this)); } }; -// Storage for LastItem data -enum ImGuiItemStatusFlags_ +// Input function taking an 'ImGuiID owner_id' argument defaults to (ImGuiKeyOwner_Any == 0) aka don't test ownership, which matches legacy behavior. +#define ImGuiKeyOwner_Any ((ImGuiID)0) // Accept key that have an owner, UNLESS a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. +#define ImGuiKeyOwner_NoOwner ((ImGuiID)-1) // Require key to have no owner. +//#define ImGuiKeyOwner_None ImGuiKeyOwner_NoOwner // We previously called this 'ImGuiKeyOwner_None' but it was inconsistent with our pattern that _None values == 0 and quite dangerous. Also using _NoOwner makes the IsKeyPressed() calls more explicit. + +typedef ImS16 ImGuiKeyRoutingIndex; + +// Routing table entry (sizeof() == 16 bytes) +struct ImGuiKeyRoutingData { - ImGuiItemStatusFlags_HoveredRect = 1 << 0, - ImGuiItemStatusFlags_HasDisplayRect = 1 << 1 + ImGuiKeyRoutingIndex NextEntryIndex; + ImU16 Mods; // Technically we'd only need 4-bits but for simplify we store ImGuiMod_ values which need 16-bits. + ImU8 RoutingCurrScore; // [DEBUG] For debug display + ImU8 RoutingNextScore; // Lower is better (0: perfect score) + ImGuiID RoutingCurr; + ImGuiID RoutingNext; + + ImGuiKeyRoutingData() { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_NoOwner; } }; -// FIXME: this is in development, not exposed/functional as a generic feature yet. -enum ImGuiLayoutType_ +// Routing table: maintain a desired owner for each possible key-chord (key + mods), and setup owner in NewFrame() when mods are matching. +// Stored in main context (1 instance) +struct ImGuiKeyRoutingTable { - ImGuiLayoutType_Vertical, - ImGuiLayoutType_Horizontal + ImGuiKeyRoutingIndex Index[ImGuiKey_NamedKey_COUNT]; // Index of first entry in Entries[] + ImVector Entries; + ImVector EntriesNext; // Double-buffer to avoid reallocation (could use a shared buffer) + + ImGuiKeyRoutingTable() { Clear(); } + void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Index); n++) Index[n] = -1; Entries.clear(); EntriesNext.clear(); } }; -enum ImGuiAxis +// This extends ImGuiKeyData but only for named keys (legacy keys don't support the new features) +// Stored in main context (1 per named key). In the future it might be merged into ImGuiKeyData. +struct ImGuiKeyOwnerData { - ImGuiAxis_None = -1, - ImGuiAxis_X = 0, - ImGuiAxis_Y = 1 + ImGuiID OwnerCurr; + ImGuiID OwnerNext; + bool LockThisFrame; // Reading this key requires explicit owner id (until end of frame). Set by ImGuiInputFlags_LockThisFrame. + bool LockUntilRelease; // Reading this key requires explicit owner id (until key is released). Set by ImGuiInputFlags_LockUntilRelease. When this is true LockThisFrame is always true as well. + + ImGuiKeyOwnerData() { OwnerCurr = OwnerNext = ImGuiKeyOwner_NoOwner; LockThisFrame = LockUntilRelease = false; } }; -enum ImGuiPlotType -{ - ImGuiPlotType_Lines, - ImGuiPlotType_Histogram +// Extend ImGuiInputFlags_ +// Flags for extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlagsPrivate_ +{ + // Flags for IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(), Shortcut() + // - Repeat mode: Repeat rate selection + ImGuiInputFlags_RepeatRateDefault = 1 << 1, // Repeat rate: Regular (default) + ImGuiInputFlags_RepeatRateNavMove = 1 << 2, // Repeat rate: Fast + ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, // Repeat rate: Faster + // - Repeat mode: Specify when repeating key pressed can be interrupted. + // - In theory ImGuiInputFlags_RepeatUntilOtherKeyPress may be a desirable default, but it would break too many behavior so everything is opt-in. + ImGuiInputFlags_RepeatUntilRelease = 1 << 4, // Stop repeating when released (default for all functions except Shortcut). This only exists to allow overriding Shortcut() default behavior. + ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, // Stop repeating when released OR if keyboard mods are changed (default for Shortcut) + ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, // Stop repeating when released OR if keyboard mods are leaving the None state. Allows going from Mod+Key to Key by releasing Mod. + ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, // Stop repeating when released OR if any other keyboard key is pressed during the repeat + + // Flags for SetKeyOwner(), SetItemKeyOwner() + // - Locking key away from non-input aware code. Locking is useful to make input-owner-aware code steal keys from non-input-owner-aware code. If all code is input-owner-aware locking would never be necessary. + ImGuiInputFlags_LockThisFrame = 1 << 20, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared at end of frame. + ImGuiInputFlags_LockUntilRelease = 1 << 21, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared when the key is released or at end of each frame if key is released. + + // - Condition for SetItemKeyOwner() + ImGuiInputFlags_CondHovered = 1 << 22, // Only set if item is hovered (default to both) + ImGuiInputFlags_CondActive = 1 << 23, // Only set if item is active (default to both) + ImGuiInputFlags_CondDefault_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + + // [Internal] Mask of which function support which flags + ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | ImGuiInputFlags_RepeatRateNavMove | ImGuiInputFlags_RepeatRateNavTweak, + ImGuiInputFlags_RepeatUntilMask_ = ImGuiInputFlags_RepeatUntilRelease | ImGuiInputFlags_RepeatUntilKeyModsChange | ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | ImGuiInputFlags_RepeatUntilOtherKeyPress, + ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_, + ImGuiInputFlags_CondMask_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + ImGuiInputFlags_RouteTypeMask_ = ImGuiInputFlags_RouteActive | ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteAlways, + ImGuiInputFlags_RouteOptionsMask_ = ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused | ImGuiInputFlags_RouteFromRootWindow, + ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, + ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, + ImGuiInputFlags_SupportedByShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_, + ImGuiInputFlags_SupportedBySetNextItemShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_ | ImGuiInputFlags_Tooltip, + ImGuiInputFlags_SupportedBySetKeyOwner = ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, + ImGuiInputFlags_SupportedBySetItemKeyOwner = ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, }; -enum ImGuiDataType +//----------------------------------------------------------------------------- +// [SECTION] Clipper support +//----------------------------------------------------------------------------- + +// Note that Max is exclusive, so perhaps should be using a Begin/End convention. +struct ImGuiListClipperRange { - ImGuiDataType_Int, - ImGuiDataType_Float, - ImGuiDataType_Float2 + int Min; + int Max; + bool PosToIndexConvert; // Begin/End are absolute position (will be converted to indices later) + ImS8 PosToIndexOffsetMin; // Add to Min after converting to indices + ImS8 PosToIndexOffsetMax; // Add to Min after converting to indices + + static ImGuiListClipperRange FromIndices(int min, int max) { ImGuiListClipperRange r = { min, max, false, 0, 0 }; return r; } + static ImGuiListClipperRange FromPositions(float y1, float y2, int off_min, int off_max) { ImGuiListClipperRange r = { (int)y1, (int)y2, true, (ImS8)off_min, (ImS8)off_max }; return r; } }; -enum ImGuiDir +// Temporary clipper data, buffers shared/reused between instances +struct ImGuiListClipperData { - ImGuiDir_None = -1, - ImGuiDir_Left = 0, - ImGuiDir_Right = 1, - ImGuiDir_Up = 2, - ImGuiDir_Down = 3, - ImGuiDir_Count_ + ImGuiListClipper* ListClipper; + float LossynessOffset; + int StepNo; + int ItemsFrozen; + ImVector Ranges; + + ImGuiListClipperData() { memset(this, 0, sizeof(*this)); } + void Reset(ImGuiListClipper* clipper) { ListClipper = clipper; StepNo = ItemsFrozen = 0; Ranges.resize(0); } }; -enum ImGuiInputSource +//----------------------------------------------------------------------------- +// [SECTION] Navigation support +//----------------------------------------------------------------------------- + +enum ImGuiActivateFlags_ { - ImGuiInputSource_None = 0, - ImGuiInputSource_Mouse, - ImGuiInputSource_Nav, - ImGuiInputSource_NavKeyboard, // Only used occasionally for storage, not tested/handled by most code - ImGuiInputSource_NavGamepad, // " - ImGuiInputSource_Count_, + ImGuiActivateFlags_None = 0, + ImGuiActivateFlags_PreferInput = 1 << 0, // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default for Enter key. + ImGuiActivateFlags_PreferTweak = 1 << 1, // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default for Space key and if keyboard is not used. + ImGuiActivateFlags_TryToPreserveState = 1 << 2, // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection) + ImGuiActivateFlags_FromTabbing = 1 << 3, // Activation requested by a tabbing request + ImGuiActivateFlags_FromShortcut = 1 << 4, // Activation requested by an item shortcut via SetNextItemShortcut() function. }; -// FIXME-NAV: Clarify/expose various repeat delay/rate -enum ImGuiInputReadMode -{ - ImGuiInputReadMode_Down, - ImGuiInputReadMode_Pressed, - ImGuiInputReadMode_Released, - ImGuiInputReadMode_Repeat, - ImGuiInputReadMode_RepeatSlow, - ImGuiInputReadMode_RepeatFast +// Early work-in-progress API for ScrollToItem() +enum ImGuiScrollFlags_ +{ + ImGuiScrollFlags_None = 0, + ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, // If item is not visible: scroll as little as possible on X axis to bring item back into view [default for X axis] + ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, // If item is not visible: scroll as little as possible on Y axis to bring item back into view [default for Y axis for windows that are already visible] + ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, // If item is not visible: scroll to make the item centered on X axis [rarely used] + ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, // If item is not visible: scroll to make the item centered on Y axis + ImGuiScrollFlags_AlwaysCenterX = 1 << 4, // Always center the result item on X axis [rarely used] + ImGuiScrollFlags_AlwaysCenterY = 1 << 5, // Always center the result item on Y axis [default for Y axis for appearing window) + ImGuiScrollFlags_NoScrollParent = 1 << 6, // Disable forwarding scrolling to parent window if required to keep item/rect visible (only scroll window the function was applied to). + ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleCenterX | ImGuiScrollFlags_AlwaysCenterX, + ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | ImGuiScrollFlags_KeepVisibleCenterY | ImGuiScrollFlags_AlwaysCenterY, }; enum ImGuiNavHighlightFlags_ { - ImGuiNavHighlightFlags_TypeDefault = 1 << 0, - ImGuiNavHighlightFlags_TypeThin = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, - ImGuiNavHighlightFlags_NoRounding = 1 << 3 + ImGuiNavHighlightFlags_None = 0, + ImGuiNavHighlightFlags_Compact = 1 << 1, // Compact highlight, no padding + ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) _even_ when using the mouse. + ImGuiNavHighlightFlags_NoRounding = 1 << 3, +}; + +enum ImGuiNavMoveFlags_ +{ + ImGuiNavMoveFlags_None = 0, + ImGuiNavMoveFlags_LoopX = 1 << 0, // On failed request, restart from opposite side + ImGuiNavMoveFlags_LoopY = 1 << 1, + ImGuiNavMoveFlags_WrapX = 1 << 2, // On failed request, request from opposite side one line down (when NavDir==right) or one line up (when NavDir==left) + ImGuiNavMoveFlags_WrapY = 1 << 3, // This is not super useful but provided for completeness + ImGuiNavMoveFlags_WrapMask_ = ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_WrapY, + ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, // Allow scoring and considering the current NavId as a move target candidate. This is used when the move source is offset (e.g. pressing PageDown actually needs to send a Up move request, if we are pressing PageDown from the bottom-most item we need to stay in place) + ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, // Store alternate result in NavMoveResultLocalVisible that only comprise elements that are already fully visible (used by PageUp/PageDown) + ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, // Force scrolling to min/max (used by Home/End) // FIXME-NAV: Aim to remove or reword, probably unnecessary + ImGuiNavMoveFlags_Forwarded = 1 << 7, + ImGuiNavMoveFlags_DebugNoResult = 1 << 8, // Dummy scoring for debug purpose, don't apply result + ImGuiNavMoveFlags_FocusApi = 1 << 9, // Requests from focus API can land/focus/activate items even if they are marked with _NoTabStop (see NavProcessItemForTabbingRequest() for details) + ImGuiNavMoveFlags_IsTabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight + ImGuiNavMoveFlags_IsPageMove = 1 << 11, // Identify a PageDown/PageUp request. + ImGuiNavMoveFlags_Activate = 1 << 12, // Activate/select target item. + ImGuiNavMoveFlags_NoSelect = 1 << 13, // Don't trigger selection by not setting g.NavJustMovedTo + ImGuiNavMoveFlags_NoSetNavHighlight = 1 << 14, // Do not alter the visible state of keyboard vs mouse nav highlight + ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, // (Experimental) Do not clear active id when applying move result }; -enum ImGuiNavDirSourceFlags_ +enum ImGuiNavLayer { - ImGuiNavDirSourceFlags_Keyboard = 1 << 0, - ImGuiNavDirSourceFlags_PadDPad = 1 << 1, - ImGuiNavDirSourceFlags_PadLStick = 1 << 2 + ImGuiNavLayer_Main = 0, // Main scrolling layer + ImGuiNavLayer_Menu = 1, // Menu layer (access with Alt) + ImGuiNavLayer_COUNT }; -enum ImGuiNavForward +// Storage for navigation query/results +struct ImGuiNavItemData +{ + ImGuiWindow* Window; // Init,Move // Best candidate window (result->ItemWindow->RootWindowForNav == request->Window) + ImGuiID ID; // Init,Move // Best candidate item ID + ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID + ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space + ImGuiItemFlags InFlags; // ????,Move // Best candidate item flags + float DistBox; // Move // Best candidate box distance to current NavId + float DistCenter; // Move // Best candidate center distance to current NavId + float DistAxial; // Move // Best candidate axial distance to current NavId + ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionUserData() value. Valid if (InFlags & ImGuiItemFlags_HasSelectionUserData) + + ImGuiNavItemData() { Clear(); } + void Clear() { Window = NULL; ID = FocusScopeId = 0; InFlags = 0; SelectionUserData = -1; DistBox = DistCenter = DistAxial = FLT_MAX; } +}; + +// Storage for PushFocusScope() +struct ImGuiFocusScopeData { - ImGuiNavForward_None, - ImGuiNavForward_ForwardQueued, - ImGuiNavForward_ForwardActive + ImGuiID ID; + ImGuiID WindowID; }; -// 2D axis aligned bounding-box -// NB: we can't rely on ImVec2 math operators being available here -struct IMGUI_API ImRect +//----------------------------------------------------------------------------- +// [SECTION] Typing-select support +//----------------------------------------------------------------------------- + +// Flags for GetTypingSelectRequest() +enum ImGuiTypingSelectFlags_ { - ImVec2 Min; // Upper-left - ImVec2 Max; // Lower-right - - ImRect() : - Min(FLT_MAX, FLT_MAX), Max(-FLT_MAX, -FLT_MAX) - {} - ImRect(const ImVec2 &min, const ImVec2 &max) : - Min(min), Max(max) - {} - ImRect(const ImVec4 &v) : - Min(v.x, v.y), Max(v.z, v.w) - {} - ImRect(float x1, float y1, float x2, float y2) : - Min(x1, y1), Max(x2, y2) - {} - - ImVec2 GetCenter() const - { - return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); - } - ImVec2 GetSize() const - { - return ImVec2(Max.x - Min.x, Max.y - Min.y); - } - float GetWidth() const - { - return Max.x - Min.x; - } - float GetHeight() const - { - return Max.y - Min.y; - } - ImVec2 GetTL() const - { - return Min; - } // Top-left - ImVec2 GetTR() const - { - return ImVec2(Max.x, Min.y); - } // Top-right - ImVec2 GetBL() const - { - return ImVec2(Min.x, Max.y); - } // Bottom-left - ImVec2 GetBR() const - { - return Max; - } // Bottom-right - bool Contains(const ImVec2 &p) const - { - return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; - } - bool Contains(const ImRect &r) const - { - return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; - } - bool Overlaps(const ImRect &r) const - { - return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; - } - void Add(const ImVec2 &p) - { - if (Min.x > p.x) - Min.x = p.x; - if (Min.y > p.y) - Min.y = p.y; - if (Max.x < p.x) - Max.x = p.x; - if (Max.y < p.y) - Max.y = p.y; - } - void Add(const ImRect &r) - { - if (Min.x > r.Min.x) - Min.x = r.Min.x; - if (Min.y > r.Min.y) - Min.y = r.Min.y; - if (Max.x < r.Max.x) - Max.x = r.Max.x; - if (Max.y < r.Max.y) - Max.y = r.Max.y; - } - void Expand(const float amount) - { - Min.x -= amount; - Min.y -= amount; - Max.x += amount; - Max.y += amount; - } - void Expand(const ImVec2 &amount) - { - Min.x -= amount.x; - Min.y -= amount.y; - Max.x += amount.x; - Max.y += amount.y; - } - void Translate(const ImVec2 &v) - { - Min.x += v.x; - Min.y += v.y; - Max.x += v.x; - Max.y += v.y; - } - void ClipWith(const ImRect &r) - { - Min = ImMax(Min, r.Min); - Max = ImMin(Max, r.Max); - } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. - void ClipWithFull(const ImRect &r) - { - Min = ImClamp(Min, r.Min, r.Max); - Max = ImClamp(Max, r.Min, r.Max); - } // Full version, ensure both points are fully clipped. - void Floor() - { - Min.x = (float) (int) Min.x; - Min.y = (float) (int) Min.y; - Max.x = (float) (int) Max.x; - Max.y = (float) (int) Max.y; - } - void FixInverted() - { - if (Min.x > Max.x) - ImSwap(Min.x, Max.x); - if (Min.y > Max.y) - ImSwap(Min.y, Max.y); - } - bool IsInverted() const - { - return Min.x > Max.x || Min.y > Max.y; - } - bool IsFinite() const - { - return Min.x != FLT_MAX; - } + ImGuiTypingSelectFlags_None = 0, + ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, // Backspace to delete character inputs. If using: ensure GetTypingSelectRequest() is not called more than once per frame (filter by e.g. focus state) + ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, // Allow "single char" search mode which is activated when pressing the same character multiple times. }; -// Stacked color modifier, backup of modified data so we can restore it -struct ImGuiColMod +// Returned by GetTypingSelectRequest(), designed to eventually be public. +struct IMGUI_API ImGuiTypingSelectRequest { - ImGuiCol Col; - ImVec4 BackupValue; + ImGuiTypingSelectFlags Flags; // Flags passed to GetTypingSelectRequest() + int SearchBufferLen; + const char* SearchBuffer; // Search buffer contents (use full string. unless SingleCharMode is set, in which case use SingleCharSize). + bool SelectRequest; // Set when buffer was modified this frame, requesting a selection. + bool SingleCharMode; // Notify when buffer contains same character repeated, to implement special mode. In this situation it preferred to not display any on-screen search indication. + ImS8 SingleCharSize; // Length in bytes of first letter codepoint (1 for ascii, 2-4 for UTF-8). If (SearchBufferLen==RepeatCharSize) only 1 letter has been input. }; -// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. -struct ImGuiStyleMod +// Storage for GetTypingSelectRequest() +struct IMGUI_API ImGuiTypingSelectState { - ImGuiStyleVar VarIdx; - union - { - int BackupInt[2]; - float BackupFloat[2]; - }; - ImGuiStyleMod(ImGuiStyleVar idx, int v) - { - VarIdx = idx; - BackupInt[0] = v; - } - ImGuiStyleMod(ImGuiStyleVar idx, float v) - { - VarIdx = idx; - BackupFloat[0] = v; - } - ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) - { - VarIdx = idx; - BackupFloat[0] = v.x; - BackupFloat[1] = v.y; - } -}; - -// Stacked data for BeginGroup()/EndGroup() -struct ImGuiGroupData -{ - ImVec2 BackupCursorPos; - ImVec2 BackupCursorMaxPos; - float BackupIndentX; - float BackupGroupOffsetX; - float BackupCurrentLineHeight; - float BackupCurrentLineTextBaseOffset; - float BackupLogLinePosY; - bool BackupActiveIdIsAlive; - bool AdvanceCursor; -}; - -// Simple column measurement currently used for MenuItem() only. This is very short-sighted/throw-away code and NOT a generic helper. -struct IMGUI_API ImGuiMenuColumns + ImGuiTypingSelectRequest Request; // User-facing data + char SearchBuffer[64]; // Search buffer: no need to make dynamic as this search is very transient. + ImGuiID FocusScope; + int LastRequestFrame = 0; + float LastRequestTime = 0.0f; + bool SingleCharModeLock = false; // After a certain single char repeat count we lock into SingleCharMode. Two benefits: 1) buffer never fill, 2) we can provide an immediate SingleChar mode without timer elapsing. + + ImGuiTypingSelectState() { memset(this, 0, sizeof(*this)); } + void Clear() { SearchBuffer[0] = 0; SingleCharModeLock = false; } // We preserve remaining data for easier debugging +}; + +//----------------------------------------------------------------------------- +// [SECTION] Columns support +//----------------------------------------------------------------------------- + +// Flags for internal's BeginColumns(). This is an obsolete API. Prefer using BeginTable() nowadays! +enum ImGuiOldColumnFlags_ +{ + ImGuiOldColumnFlags_None = 0, + ImGuiOldColumnFlags_NoBorder = 1 << 0, // Disable column dividers + ImGuiOldColumnFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers + ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns + ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window + ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, // Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, + //ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, + //ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, + //ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, + //ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, + //ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize, +#endif +}; + +struct ImGuiOldColumnData { - int Count; - float Spacing; - float Width, NextWidth; - float Pos[4], NextWidths[4]; + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiOldColumnFlags Flags; // Not exposed + ImRect ClipRect; - ImGuiMenuColumns(); - void Update(int count, float spacing, bool clear); - float DeclColumns(float w0, float w1, float w2); - float CalcExtraSpace(float avail_w); + ImGuiOldColumnData() { memset(this, 0, sizeof(*this)); } }; -// Internal state of the currently focused/edited text input box -struct IMGUI_API ImGuiTextEditState -{ - ImGuiID Id; // widget id owning the text state - ImVector Text; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. - ImVector InitialText; // backup of end-user buffer at the time of focus (in UTF-8, unaltered) - ImVector TempTextBuffer; - int CurLenA, CurLenW; // we need to maintain our buffer length in both UTF-8 and wchar format. - int BufSizeA; // end-user buffer size - float ScrollX; - ImGuiStb::STB_TexteditState StbState; - float CursorAnim; - bool CursorFollow; - bool SelectedAllMouseLock; - - ImGuiTextEditState() - { - memset(this, 0, sizeof(*this)); - } - void CursorAnimReset() - { - CursorAnim = -0.30f; - } // After a user-input the cursor stays on for a while without blinking - void CursorClamp() - { - StbState.cursor = ImMin(StbState.cursor, CurLenW); - StbState.select_start = ImMin(StbState.select_start, CurLenW); - StbState.select_end = ImMin(StbState.select_end, CurLenW); - } - bool HasSelection() const - { - return StbState.select_start != StbState.select_end; - } - void ClearSelection() - { - StbState.select_start = StbState.select_end = StbState.cursor; - } - void SelectAll() - { - StbState.select_start = 0; - StbState.cursor = StbState.select_end = CurLenW; - StbState.has_preferred_x = false; - } - void OnKeyPressed(int key); -}; - -// Data saved in imgui.ini file +struct ImGuiOldColumns +{ + ImGuiID ID; + ImGuiOldColumnFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float OffMinX, OffMaxX; // Offsets from HostWorkRect.Min.x + float LineMinY, LineMaxY; + float HostCursorPosY; // Backup of CursorPos at the time of BeginColumns() + float HostCursorMaxPosX; // Backup of CursorMaxPos at the time of BeginColumns() + ImRect HostInitialClipRect; // Backup of ClipRect at the time of BeginColumns() + ImRect HostBackupClipRect; // Backup of ClipRect during PushColumnsBackground()/PopColumnsBackground() + ImRect HostBackupParentWorkRect;//Backup of WorkRect at the time of BeginColumns() + ImVector Columns; + ImDrawListSplitter Splitter; + + ImGuiOldColumns() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Box-select support +//----------------------------------------------------------------------------- + +struct ImGuiBoxSelectState +{ + // Active box-selection data (persistent, 1 active at a time) + ImGuiID ID; + bool IsActive; + bool IsStarting; + bool IsStartedFromVoid; // Starting click was not from an item. + bool IsStartedSetNavIdOnce; + bool RequestClear; + ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) + ImVec2 EndPosRel; // End position in window-contents relative space + ImVec2 ScrollAccum; // Scrolling accumulator (to behave at high-frame spaces) + ImGuiWindow* Window; + + // Temporary/Transient data + bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. + ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) + ImRect BoxSelectRectCurr; + + ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Multi-select support +//----------------------------------------------------------------------------- + +// We always assume that -1 is an invalid value (which works for indices and pointers) +#define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) + +// Temporary storage for multi-select +struct IMGUI_API ImGuiMultiSelectTempData +{ + ImGuiMultiSelectIO IO; // MUST BE FIRST FIELD. Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. + ImGuiMultiSelectState* Storage; + ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) + ImGuiMultiSelectFlags Flags; + ImVec2 ScopeRectMin; + ImVec2 BackupCursorMaxPos; + ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. + ImGuiID BoxSelectId; + ImGuiKeyChord KeyMods; + ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. + bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. + bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. + bool IsKeyboardSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + bool NavIdPassedBy; + bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. + bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. + + ImGuiMultiSelectTempData() { Clear(); } + void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. + void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = ImGuiSelectionUserData_Invalid; IO.NavIdSelected = IO.RangeSrcReset = false; } +}; + +// Persistent storage for multi-select (as long as selection is alive) +struct IMGUI_API ImGuiMultiSelectState +{ + ImGuiWindow* Window; + ImGuiID ID; + int LastFrameActive; // Last used frame-count, for GC. + int LastSelectionSize; // Set by BeginMultiSelect() based on optional info provided by user. May be -1 if unknown. + ImS8 RangeSelected; // -1 (don't have) or true/false + ImS8 NavIdSelected; // -1 (don't have) or true/false + ImGuiSelectionUserData RangeSrcItem; // + ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) + + ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = LastSelectionSize = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Docking support +//----------------------------------------------------------------------------- + +#ifdef IMGUI_HAS_DOCK +// +#endif // #ifdef IMGUI_HAS_DOCK + +//----------------------------------------------------------------------------- +// [SECTION] Viewport support +//----------------------------------------------------------------------------- + +// ImGuiViewport Private/Internals fields (cardinal sin: we are using inheritance!) +// Every instance of ImGuiViewport is in fact a ImGuiViewportP. +struct ImGuiViewportP : public ImGuiViewport +{ + int BgFgDrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used + ImDrawList* BgFgDrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. + ImDrawData DrawDataP; + ImDrawDataBuilder DrawDataBuilder; // Temporary data while building final ImDrawData + + // Per-viewport work area + // - Insets are >= 0.0f values, distance from viewport corners to work area. + // - BeginMainMenuBar() and DockspaceOverViewport() tend to use work area to avoid stepping over existing contents. + // - Generally 'safeAreaInsets' in iOS land, 'DisplayCutout' in Android land. + ImVec2 WorkInsetMin; // Work Area inset locked for the frame. GetWorkRect() always fits within GetMainRect(). + ImVec2 WorkInsetMax; // " + ImVec2 BuildWorkInsetMin; // Work Area inset accumulator for current frame, to become next frame's WorkInset + ImVec2 BuildWorkInsetMax; // " + + ImGuiViewportP() { BgFgDrawListsLastFrame[0] = BgFgDrawListsLastFrame[1] = -1; BgFgDrawLists[0] = BgFgDrawLists[1] = NULL; } + ~ImGuiViewportP() { if (BgFgDrawLists[0]) IM_DELETE(BgFgDrawLists[0]); if (BgFgDrawLists[1]) IM_DELETE(BgFgDrawLists[1]); } + + // Calculate work rect pos/size given a set of offset (we have 1 pair of offset for rect locked from last frame data, and 1 pair for currently building rect) + ImVec2 CalcWorkRectPos(const ImVec2& inset_min) const { return ImVec2(Pos.x + inset_min.x, Pos.y + inset_min.y); } + ImVec2 CalcWorkRectSize(const ImVec2& inset_min, const ImVec2& inset_max) const { return ImVec2(ImMax(0.0f, Size.x - inset_min.x - inset_max.x), ImMax(0.0f, Size.y - inset_min.y - inset_max.y)); } + void UpdateWorkRect() { WorkPos = CalcWorkRectPos(WorkInsetMin); WorkSize = CalcWorkRectSize(WorkInsetMin, WorkInsetMax); } // Update public fields + + // Helpers to retrieve ImRect (we don't need to store BuildWorkRect as every access tend to change it, hence the code asymmetry) + ImRect GetMainRect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + ImRect GetWorkRect() const { return ImRect(WorkPos.x, WorkPos.y, WorkPos.x + WorkSize.x, WorkPos.y + WorkSize.y); } + ImRect GetBuildWorkRect() const { ImVec2 pos = CalcWorkRectPos(BuildWorkInsetMin); ImVec2 size = CalcWorkRectSize(BuildWorkInsetMin, BuildWorkInsetMax); return ImRect(pos.x, pos.y, pos.x + size.x, pos.y + size.y); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Settings support +//----------------------------------------------------------------------------- + +// Windows data saved in imgui.ini file +// Because we never destroy or rename ImGuiWindowSettings, we can store the names in a separate buffer easily. +// (this is designed to be stored in a ImChunkStream buffer, with the variable-length Name following our structure) struct ImGuiWindowSettings { - char *Name; - ImGuiID Id; - ImVec2 Pos; - ImVec2 Size; - bool Collapsed; + ImGuiID ID; + ImVec2ih Pos; + ImVec2ih Size; + bool Collapsed; + bool IsChild; + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + bool WantDelete; // Set to invalidate/delete the settings entry - ImGuiWindowSettings() - { - Name = NULL; - Id = 0; - Pos = Size = ImVec2(0, 0); - Collapsed = false; - } + ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); } + char* GetName() { return (char*)(this + 1); } }; struct ImGuiSettingsHandler { - const char *TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' - ImGuiID TypeHash; // == ImHash(TypeName, 0, 0) - void *(*ReadOpenFn)(ImGuiContext *ctx, ImGuiSettingsHandler *handler, const char *name); - void (*ReadLineFn)(ImGuiContext *ctx, ImGuiSettingsHandler *handler, void *entry, const char *line); - void (*WriteAllFn)(ImGuiContext *ctx, ImGuiSettingsHandler *handler, ImGuiTextBuffer *out_buf); - void *UserData; - - ImGuiSettingsHandler() - { - memset(this, 0, sizeof(*this)); - } -}; - -// Storage for current popup stack -struct ImGuiPopupRef -{ - ImGuiID PopupId; // Set on OpenPopup() - ImGuiWindow *Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() - ImGuiWindow *ParentWindow; // Set on OpenPopup() - int OpenFrameCount; // Set on OpenPopup() - ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differenciate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) - ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) - ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup -}; - -struct ImGuiColumnData -{ - float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) - float OffsetNormBeforeResize; - ImGuiColumnsFlags Flags; // Not exposed - ImRect ClipRect; - - ImGuiColumnData() - { - OffsetNorm = OffsetNormBeforeResize = 0.0f; - Flags = 0; - } -}; - -struct ImGuiColumnsSet -{ - ImGuiID ID; - ImGuiColumnsFlags Flags; - bool IsFirstFrame; - bool IsBeingResized; - int Current; - int Count; - float MinX, MaxX; - float StartPosY; - float StartMaxPosX; // Backup of CursorMaxPos - float CellMinY, CellMaxY; - ImVector Columns; - - ImGuiColumnsSet() - { - Clear(); - } - void Clear() - { - ID = 0; - Flags = 0; - IsFirstFrame = false; - IsBeingResized = false; - Current = 0; - Count = 1; - MinX = MaxX = 0.0f; - StartPosY = 0.0f; - StartMaxPosX = 0.0f; - CellMinY = CellMaxY = 0.0f; - Columns.clear(); - } + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHashStr(TypeName) + void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Clear all settings data + void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called before reading (in registration order) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); // Read: Called when entering into a new ini entry e.g. "[Window][Name]" + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry + void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called after reading (in registration order) + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } }; -struct IMGUI_API ImDrawListSharedData +//----------------------------------------------------------------------------- +// [SECTION] Localization support +//----------------------------------------------------------------------------- + +// This is experimental and not officially supported, it'll probably fall short of features, if/when it does we may backtrack. +enum ImGuiLocKey : int +{ + ImGuiLocKey_VersionStr, + ImGuiLocKey_TableSizeOne, + ImGuiLocKey_TableSizeAllFit, + ImGuiLocKey_TableSizeAllDefault, + ImGuiLocKey_TableResetOrder, + ImGuiLocKey_WindowingMainMenuBar, + ImGuiLocKey_WindowingPopup, + ImGuiLocKey_WindowingUntitled, + ImGuiLocKey_CopyLink, + ImGuiLocKey_COUNT +}; + +struct ImGuiLocEntry { - ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas - ImFont *Font; // Current/default font (optional, for simplified AddText overload) - float FontSize; // Current/default font size (optional, for simplified AddText overload) - float CurveTessellationTol; - ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + ImGuiLocKey Key; + const char* Text; +}; - // Const data - // FIXME: Bake rounded corners fill/borders in atlas - ImVec2 CircleVtx12[12]; - ImDrawListSharedData(); +//----------------------------------------------------------------------------- +// [SECTION] Metrics, Debug Tools +//----------------------------------------------------------------------------- + +enum ImGuiDebugLogFlags_ +{ + // Event types + ImGuiDebugLogFlags_None = 0, + ImGuiDebugLogFlags_EventActiveId = 1 << 0, + ImGuiDebugLogFlags_EventFocus = 1 << 1, + ImGuiDebugLogFlags_EventPopup = 1 << 2, + ImGuiDebugLogFlags_EventNav = 1 << 3, + ImGuiDebugLogFlags_EventClipper = 1 << 4, + ImGuiDebugLogFlags_EventSelection = 1 << 5, + ImGuiDebugLogFlags_EventIO = 1 << 6, + ImGuiDebugLogFlags_EventInputRouting = 1 << 7, + + ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting, + ImGuiDebugLogFlags_OutputToTTY = 1 << 20, // Also send output to TTY + ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, // Also send output to Test Engine }; -struct ImDrawDataBuilder +struct ImGuiDebugAllocEntry { - ImVector Layers[2]; // Global layers for: regular, tooltip - - void Clear() - { - for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) - Layers[n].resize(0); - } - void ClearFreeMemory() - { - for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) - Layers[n].clear(); - } - IMGUI_API void FlattenIntoSingleLayer(); -}; - -struct ImGuiNavMoveResult -{ - ImGuiID ID; // Best candidate - ImGuiID ParentID; // Best candidate window->IDStack.back() - to compare context - ImGuiWindow *Window; // Best candidate window - float DistBox; // Best candidate box distance to current NavId - float DistCenter; // Best candidate center distance to current NavId - float DistAxial; - ImRect RectRel; // Best candidate bounding box in window relative space - - ImGuiNavMoveResult() - { - Clear(); - } - void Clear() - { - ID = ParentID = 0; - Window = NULL; - DistBox = DistCenter = DistAxial = FLT_MAX; - RectRel = ImRect(); - } + int FrameCount; + ImS16 AllocCount; + ImS16 FreeCount; }; -// Storage for SetNexWindow** functions -struct ImGuiNextWindowData +struct ImGuiDebugAllocInfo +{ + int TotalAllocCount; // Number of call to MemAlloc(). + int TotalFreeCount; + ImS16 LastEntriesIdx; // Current index in buffer + ImGuiDebugAllocEntry LastEntriesBuf[6]; // Track last 6 frames that had allocations + + ImGuiDebugAllocInfo() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiMetricsConfig +{ + bool ShowDebugLog = false; + bool ShowIDStackTool = false; + bool ShowWindowsRects = false; + bool ShowWindowsBeginOrder = false; + bool ShowTablesRects = false; + bool ShowDrawCmdMesh = true; + bool ShowDrawCmdBoundingBoxes = true; + bool ShowTextEncodingViewer = false; + bool ShowAtlasTintedWithTextColor = false; + int ShowWindowsRectsType = -1; + int ShowTablesRectsType = -1; + int HighlightMonitorIdx = -1; + ImGuiID HighlightViewportID = 0; +}; + +struct ImGuiStackLevelInfo +{ + ImGuiID ID; + ImS8 QueryFrameCount; // >= 1: Query in progress + bool QuerySuccess; // Obtained result from DebugHookIdInfo() + ImGuiDataType DataType : 8; + char Desc[57]; // Arbitrarily sized buffer to hold a result (FIXME: could replace Results[] with a chunk stream?) FIXME: Now that we added CTRL+C this should be fixed. + + ImGuiStackLevelInfo() { memset(this, 0, sizeof(*this)); } +}; + +// State for ID Stack tool queries +struct ImGuiIDStackTool +{ + int LastActiveFrame; + int StackLevel; // -1: query stack and resize Results, >= 0: individual stack level + ImGuiID QueryId; // ID to query details for + ImVector Results; + bool CopyToClipboardOnCtrlC; + float CopyToClipboardLastTime; + + ImGuiIDStackTool() { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Generic context hooks +//----------------------------------------------------------------------------- + +typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, ImGuiContextHook* hook); +enum ImGuiContextHookType { ImGuiContextHookType_NewFramePre, ImGuiContextHookType_NewFramePost, ImGuiContextHookType_EndFramePre, ImGuiContextHookType_EndFramePost, ImGuiContextHookType_RenderPre, ImGuiContextHookType_RenderPost, ImGuiContextHookType_Shutdown, ImGuiContextHookType_PendingRemoval_ }; + +struct ImGuiContextHook { - ImGuiCond PosCond; - ImGuiCond SizeCond; - ImGuiCond ContentSizeCond; - ImGuiCond CollapsedCond; - ImGuiCond SizeConstraintCond; - ImGuiCond FocusCond; - ImGuiCond BgAlphaCond; - ImVec2 PosVal; - ImVec2 PosPivotVal; - ImVec2 SizeVal; - ImVec2 ContentSizeVal; - bool CollapsedVal; - ImRect SizeConstraintRect; // Valid if 'SetNextWindowSizeConstraint' is true - ImGuiSizeCallback SizeCallback; - void *SizeCallbackUserData; - float BgAlphaVal; - - ImGuiNextWindowData() - { - PosCond = SizeCond = ContentSizeCond = CollapsedCond = SizeConstraintCond = FocusCond = BgAlphaCond = 0; - PosVal = PosPivotVal = SizeVal = ImVec2(0.0f, 0.0f); - ContentSizeVal = ImVec2(0.0f, 0.0f); - CollapsedVal = false; - SizeConstraintRect = ImRect(); - SizeCallback = NULL; - SizeCallbackUserData = NULL; - BgAlphaVal = FLT_MAX; - } - - void Clear() - { - PosCond = SizeCond = ContentSizeCond = CollapsedCond = SizeConstraintCond = FocusCond = BgAlphaCond = 0; - } -}; - -// Main state for ImGui + ImGuiID HookId; // A unique ID assigned by AddContextHook() + ImGuiContextHookType Type; + ImGuiID Owner; + ImGuiContextHookCallback Callback; + void* UserData; + + ImGuiContextHook() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiContext (main Dear ImGui context) +//----------------------------------------------------------------------------- + struct ImGuiContext { - bool Initialized; - bool FontAtlasOwnedByContext; // Io.Fonts-> is owned by the ImGuiContext and will be destructed along with it. - ImGuiIO IO; - ImGuiStyle Style; - ImFont *Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() - float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. - float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. - ImDrawListSharedData DrawListSharedData; - - float Time; - int FrameCount; - int FrameCountEnded; - int FrameCountRendered; - ImVector Windows; - ImVector WindowsSortBuffer; - ImVector CurrentWindowStack; - ImGuiStorage WindowsById; - int WindowsActiveCount; - ImGuiWindow *CurrentWindow; // Being drawn into - ImGuiWindow *HoveredWindow; // Will catch mouse inputs - ImGuiWindow *HoveredRootWindow; // Will catch mouse inputs (for focus/move only) - ImGuiID HoveredId; // Hovered widget - bool HoveredIdAllowOverlap; - ImGuiID HoveredIdPreviousFrame; - float HoveredIdTimer; - ImGuiID ActiveId; // Active widget - ImGuiID ActiveIdPreviousFrame; - float ActiveIdTimer; - bool ActiveIdIsAlive; // Active widget has been seen this frame - bool ActiveIdIsJustActivated; // Set at the time of activation for one frame - bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) - int ActiveIdAllowNavDirFlags; // Active widget allows using directional navigation (e.g. can activate a button and move away from it) - ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) - ImGuiWindow *ActiveIdWindow; - ImGuiInputSource ActiveIdSource; // Activating with mouse or nav (gamepad/keyboard) - ImGuiWindow *MovingWindow; // Track the window we clicked on (in order to preserve focus). The actually window that is moved is generally MovingWindow->RootWindow. - ImVector ColorModifiers; // Stack for PushStyleColor()/PopStyleColor() - ImVector StyleModifiers; // Stack for PushStyleVar()/PopStyleVar() - ImVector FontStack; // Stack for PushFont()/PopFont() - ImVector OpenPopupStack; // Which popups are open (persistent) - ImVector CurrentPopupStack; // Which level of BeginPopup() we are in (reset every frame) - ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions - bool NextTreeNodeOpenVal; // Storage for SetNextTreeNode** functions - ImGuiCond NextTreeNodeOpenCond; - - // Navigation data (for gamepad/keyboard) - ImGuiWindow *NavWindow; // Focused window for navigation. Could be called 'FocusWindow' - ImGuiID NavId; // Focused item for navigation - ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0, also set when calling ActivateItem() - ImGuiID NavActivateDownId; // ~~ IsNavInputDown(ImGuiNavInput_Activate) ? NavId : 0 - ImGuiID NavActivatePressedId; // ~~ IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0 - ImGuiID NavInputId; // ~~ IsNavInputPressed(ImGuiNavInput_Input) ? NavId : 0 - ImGuiID NavJustTabbedId; // Just tabbed to this id. - ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame - ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest) - ImRect NavScoringRectScreen; // Rectangle used for scoring, in screen space. Based of window->DC.NavRefRectRel[], modified for directional navigation scoring. - int NavScoringCount; // Metrics for debugging - ImGuiWindow *NavWindowingTarget; // When selecting a window (holding Menu+FocusPrev/Next, or equivalent of CTRL-TAB) this window is temporarily displayed front-most. - float NavWindowingHighlightTimer; - float NavWindowingHighlightAlpha; - bool NavWindowingToggleLayer; - ImGuiInputSource NavWindowingInputSource; // Gamepad or keyboard mode - int NavLayer; // Layer we are navigating on. For now the system is hard-coded for 0=main contents and 1=menu/title bar, may expose layers later. - int NavIdTabCounter; // == NavWindow->DC.FocusIdxTabCounter at time of NavId processing - bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRefRectRel is valid - bool NavMousePosDirty; // When set we will update mouse position if (NavFlags & ImGuiNavFlags_MoveMouse) if set (NB: this not enabled by default) - bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (nb: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) - bool NavDisableMouseHover; // When user starts using gamepad/keyboard, we hide mouse hovering highlight until mouse is touched again. - bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest - bool NavInitRequest; // Init request for appearing window to select first item - bool NavInitRequestFromMove; - ImGuiID NavInitResultId; - ImRect NavInitResultRectRel; - bool NavMoveFromClampedRefRect; // Set by manual scrolling, if we scroll to a point where NavId isn't visible we reset navigation from visible items - bool NavMoveRequest; // Move request for this frame - ImGuiNavForward NavMoveRequestForward; // None / ForwardQueued / ForwardActive (this is used to navigate sibling parent menus from a child menu) - ImGuiDir NavMoveDir, NavMoveDirLast; // Direction of the move request (left/right/up/down), direction of the previous move request - ImGuiNavMoveResult NavMoveResultLocal; // Best move request candidate within NavWindow - ImGuiNavMoveResult NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using the NavFlattened flag) - - // Render - ImDrawData DrawData; // Main ImDrawData instance to pass render information to the user - ImDrawDataBuilder DrawDataBuilder; - float ModalWindowDarkeningRatio; - ImDrawList OverlayDrawList; // Optional software render of mouse cursors, if io.MouseDrawCursor is set + a few debug overlays - ImGuiMouseCursor MouseCursor; - - // Drag and Drop - bool DragDropActive; - ImGuiDragDropFlags DragDropSourceFlags; - int DragDropMouseButton; - ImGuiPayload DragDropPayload; - ImRect DragDropTargetRect; - ImGuiID DragDropTargetId; - float DragDropAcceptIdCurrRectSurface; - ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) - ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) - int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source - ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly - unsigned char DragDropPayloadBufLocal[8]; - - // Widget state - ImGuiTextEditState InputTextState; - ImFont InputTextPasswordFont; - ImGuiID ScalarAsInputTextId; // Temporary text input when CTRL+clicking on a slider, etc. - ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets - ImVec4 ColorPickerRef; - float DragCurrentValue; // Currently dragged value, always float, not rounded by end-user precision settings - ImVec2 DragLastMouseDelta; - float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio - float DragSpeedScaleSlow; - float DragSpeedScaleFast; - ImVec2 ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? - int TooltipOverrideCount; - ImVector PrivateClipboard; // If no custom clipboard handler is defined - ImVec2 OsImePosRequest, OsImePosSet; // Cursor position request & last passed to the OS Input Method Editor - - // Settings - bool SettingsLoaded; - float SettingsDirtyTimer; // Save .ini Settings on disk when time reaches zero - ImVector SettingsWindows; // .ini settings for ImGuiWindow - ImVector SettingsHandlers; // List of .ini settings handlers - - // Logging - bool LogEnabled; - FILE *LogFile; // If != NULL log to stdout/ file - ImGuiTextBuffer *LogClipboard; // Else log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. - int LogStartDepth; - int LogAutoExpandMaxDepth; - - // Misc - float FramerateSecPerFrame[120]; // calculate estimate of framerate for user - int FramerateSecPerFrameIdx; - float FramerateSecPerFrameAccum; - int WantCaptureMouseNextFrame; // explicit capture via CaptureInputs() sets those flags - int WantCaptureKeyboardNextFrame; - int WantTextInputNextFrame; - char TempBuffer[1024 * 3 + 1]; // temporary text buffer - - ImGuiContext(ImFontAtlas *shared_font_atlas) : - OverlayDrawList(NULL) - { - Initialized = false; - Font = NULL; - FontSize = FontBaseSize = 0.0f; - FontAtlasOwnedByContext = shared_font_atlas ? false : true; - IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); - - Time = 0.0f; - FrameCount = 0; - FrameCountEnded = FrameCountRendered = -1; - WindowsActiveCount = 0; - CurrentWindow = NULL; - HoveredWindow = NULL; - HoveredRootWindow = NULL; - HoveredId = 0; - HoveredIdAllowOverlap = false; - HoveredIdPreviousFrame = 0; - HoveredIdTimer = 0.0f; - ActiveId = 0; - ActiveIdPreviousFrame = 0; - ActiveIdTimer = 0.0f; - ActiveIdIsAlive = false; - ActiveIdIsJustActivated = false; - ActiveIdAllowOverlap = false; - ActiveIdAllowNavDirFlags = 0; - ActiveIdClickOffset = ImVec2(-1, -1); - ActiveIdWindow = NULL; - ActiveIdSource = ImGuiInputSource_None; - MovingWindow = NULL; - NextTreeNodeOpenVal = false; - NextTreeNodeOpenCond = 0; - - NavWindow = NULL; - NavId = NavActivateId = NavActivateDownId = NavActivatePressedId = NavInputId = 0; - NavJustTabbedId = NavJustMovedToId = NavNextActivateId = 0; - NavScoringRectScreen = ImRect(); - NavScoringCount = 0; - NavWindowingTarget = NULL; - NavWindowingHighlightTimer = NavWindowingHighlightAlpha = 0.0f; - NavWindowingToggleLayer = false; - NavWindowingInputSource = ImGuiInputSource_None; - NavLayer = 0; - NavIdTabCounter = INT_MAX; - NavIdIsAlive = false; - NavMousePosDirty = false; - NavDisableHighlight = true; - NavDisableMouseHover = false; - NavAnyRequest = false; - NavInitRequest = false; - NavInitRequestFromMove = false; - NavInitResultId = 0; - NavMoveFromClampedRefRect = false; - NavMoveRequest = false; - NavMoveRequestForward = ImGuiNavForward_None; - NavMoveDir = NavMoveDirLast = ImGuiDir_None; - - ModalWindowDarkeningRatio = 0.0f; - OverlayDrawList._Data = &DrawListSharedData; - OverlayDrawList._OwnerName = "##Overlay"; // Give it a name for debugging - MouseCursor = ImGuiMouseCursor_Arrow; - - DragDropActive = false; - DragDropSourceFlags = 0; - DragDropMouseButton = -1; - DragDropTargetId = 0; - DragDropAcceptIdCurrRectSurface = 0.0f; - DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; - DragDropAcceptFrameCount = -1; - memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); - - ScalarAsInputTextId = 0; - ColorEditOptions = ImGuiColorEditFlags__OptionsDefault; - DragCurrentValue = 0.0f; - DragLastMouseDelta = ImVec2(0.0f, 0.0f); - DragSpeedDefaultRatio = 1.0f / 100.0f; - DragSpeedScaleSlow = 1.0f / 100.0f; - DragSpeedScaleFast = 10.0f; - ScrollbarClickDeltaToGrabCenter = ImVec2(0.0f, 0.0f); - TooltipOverrideCount = 0; - OsImePosRequest = OsImePosSet = ImVec2(-1.0f, -1.0f); - - SettingsLoaded = false; - SettingsDirtyTimer = 0.0f; - - LogEnabled = false; - LogFile = NULL; - LogClipboard = NULL; - LogStartDepth = 0; - LogAutoExpandMaxDepth = 2; - - memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); - FramerateSecPerFrameIdx = 0; - FramerateSecPerFrameAccum = 0.0f; - WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; - memset(TempBuffer, 0, sizeof(TempBuffer)); - } -}; - -// Transient per-window flags, reset at the beginning of the frame. For child window, inherited from parent on first Begin(). -// This is going to be exposed in imgui.h when stabilized enough. -enum ImGuiItemFlags_ -{ - ImGuiItemFlags_AllowKeyboardFocus = 1 << 0, // true - ImGuiItemFlags_ButtonRepeat = 1 << 1, // false // Button() will return true multiple times based on io.KeyRepeatDelay and io.KeyRepeatRate settings. - ImGuiItemFlags_Disabled = 1 << 2, // false // FIXME-WIP: Disable interactions but doesn't affect visuals. Should be: grey out and disable interactions with widgets that affect data + view widgets (WIP) - ImGuiItemFlags_NoNav = 1 << 3, // false - ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, // false - ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, // false // MenuItem/Selectable() automatically closes current Popup window - ImGuiItemFlags_Default_ = ImGuiItemFlags_AllowKeyboardFocus -}; - -// Transient per-window data, reset at the beginning of the frame -// FIXME: That's theory, in practice the delimitation between ImGuiWindow and ImGuiDrawContext is quite tenuous and could be reconsidered. -struct IMGUI_API ImGuiDrawContext -{ - ImVec2 CursorPos; - ImVec2 CursorPosPrevLine; - ImVec2 CursorStartPos; - ImVec2 CursorMaxPos; // Used to implicitly calculate the size of our contents, always growing during the frame. Turned into window->SizeContents at the beginning of next frame - float CurrentLineHeight; - float CurrentLineTextBaseOffset; - float PrevLineHeight; - float PrevLineTextBaseOffset; - float LogLinePosY; - int TreeDepth; - ImU32 TreeDepthMayJumpToParentOnPop; // Store a copy of !g.NavIdIsAlive for TreeDepth 0..31 - ImGuiID LastItemId; - ImGuiItemStatusFlags LastItemStatusFlags; - ImRect LastItemRect; // Interaction rect - ImRect LastItemDisplayRect; // End-user display rect (only valid if LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) - bool NavHideHighlightOneFrame; - bool NavHasScroll; // Set when scrolling can be used (ScrollMax > 0.0f) - int NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) - int NavLayerCurrentMask; // = (1 << NavLayerCurrent) used by ItemAdd prior to clipping. - int NavLayerActiveMask; // Which layer have been written to (result from previous frame) - int NavLayerActiveMaskNext; // Which layer have been written to (buffer for current frame) - bool MenuBarAppending; // FIXME: Remove this - float MenuBarOffsetX; - ImVector ChildWindows; - ImGuiStorage *StateStorage; - ImGuiLayoutType LayoutType; - ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() - - // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. - ImGuiItemFlags ItemFlags; // == ItemFlagsStack.back() [empty == ImGuiItemFlags_Default] - float ItemWidth; // == ItemWidthStack.back(). 0.0: default, >0.0: width in pixels, <0.0: align xx pixels to the right of window - float TextWrapPos; // == TextWrapPosStack.back() [empty == -1.0f] - ImVector ItemFlagsStack; - ImVector ItemWidthStack; - ImVector TextWrapPosStack; - ImVector GroupStack; - int StackSizesBackup[6]; // Store size of various stacks for asserting - - float IndentX; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) - float GroupOffsetX; - float ColumnsOffsetX; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. - ImGuiColumnsSet *ColumnsSet; // Current columns set - - ImGuiDrawContext() - { - CursorPos = CursorPosPrevLine = CursorStartPos = CursorMaxPos = ImVec2(0.0f, 0.0f); - CurrentLineHeight = PrevLineHeight = 0.0f; - CurrentLineTextBaseOffset = PrevLineTextBaseOffset = 0.0f; - LogLinePosY = -1.0f; - TreeDepth = 0; - TreeDepthMayJumpToParentOnPop = 0x00; - LastItemId = 0; - LastItemStatusFlags = 0; - LastItemRect = LastItemDisplayRect = ImRect(); - NavHideHighlightOneFrame = false; - NavHasScroll = false; - NavLayerActiveMask = NavLayerActiveMaskNext = 0x00; - NavLayerCurrent = 0; - NavLayerCurrentMask = 1 << 0; - MenuBarAppending = false; - MenuBarOffsetX = 0.0f; - StateStorage = NULL; - LayoutType = ParentLayoutType = ImGuiLayoutType_Vertical; - ItemWidth = 0.0f; - ItemFlags = ImGuiItemFlags_Default_; - TextWrapPos = -1.0f; - memset(StackSizesBackup, 0, sizeof(StackSizesBackup)); - - IndentX = 0.0f; - GroupOffsetX = 0.0f; - ColumnsOffsetX = 0.0f; - ColumnsSet = NULL; - } -}; - -// Windows data + bool Initialized; + bool FontAtlasOwnedByContext; // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it. + ImGuiIO IO; + ImGuiPlatformIO PlatformIO; + ImGuiStyle Style; + ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() + float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. + float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. + float FontScale; // == FontSize / Font->FontSize + float CurrentDpiScale; // Current window/viewport DpiScale + ImDrawListSharedData DrawListSharedData; + double Time; + int FrameCount; + int FrameCountEnded; + int FrameCountRendered; + bool WithinFrameScope; // Set by NewFrame(), cleared by EndFrame() + bool WithinFrameScopeWithImplicitWindow; // Set by NewFrame(), cleared by EndFrame() when the implicit debug window has been pushed + bool WithinEndChild; // Set within EndChild() + bool GcCompactAll; // Request full GC + bool TestEngineHookItems; // Will call test engine hooks: ImGuiTestEngineHook_ItemAdd(), ImGuiTestEngineHook_ItemInfo(), ImGuiTestEngineHook_Log() + void* TestEngine; // Test engine user data + char ContextName[16]; // Storage for a context name (to facilitate debugging multi-context setups) + + // Inputs + ImVector InputEventsQueue; // Input events which will be trickled/written into IO structure. + ImVector InputEventsTrail; // Past input events processed in NewFrame(). This is to allow domain-specific application to access e.g mouse/pen trail. + ImGuiMouseSource InputEventsNextMouseSource; + ImU32 InputEventsNextEventId; + + // Windows state + ImVector Windows; // Windows, sorted in display order, back to front + ImVector WindowsFocusOrder; // Root windows, sorted in focus order, back to front. + ImVector WindowsTempSortBuffer; // Temporary buffer used in EndFrame() to reorder windows so parents are kept before their child + ImVector CurrentWindowStack; + ImGuiStorage WindowsById; // Map window's ImGuiID to ImGuiWindow* + int WindowsActiveCount; // Number of unique windows submitted by frame + ImVec2 WindowsHoverPadding; // Padding around resizable windows for which hovering on counts as hovering the window == ImMax(style.TouchExtraPadding, WINDOWS_HOVER_PADDING). + ImGuiID DebugBreakInWindow; // Set to break in Begin() call. + ImGuiWindow* CurrentWindow; // Window being drawn into + ImGuiWindow* HoveredWindow; // Window the mouse is hovering. Will typically catch mouse inputs. + ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. + ImGuiWindow* HoveredWindowBeforeClear; // Window the mouse is hovering. Filled even with _NoMouse. This is currently useful for multi-context compositors. + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindow. + ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. + ImVec2 WheelingWindowRefMousePos; + int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL + int WheelingWindowScrolledFrame; + float WheelingWindowReleaseTimer; + ImVec2 WheelingWindowWheelRemainder; + ImVec2 WheelingAxisAvg; + + // Item/widgets state and tracking information + ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] + ImGuiID HoveredId; // Hovered widget, filled during the frame + ImGuiID HoveredIdPreviousFrame; + float HoveredIdTimer; // Measure contiguous hovering time + float HoveredIdNotActiveTimer; // Measure contiguous hovering time where the item has not been active + bool HoveredIdAllowOverlap; + bool HoveredIdIsDisabled; // At least one widget passed the rect test, but has been discarded by disabled flag or popup inhibit. May be true even if HoveredId == 0. + bool ItemUnclipByLog; // Disable ItemAdd() clipping, essentially a memory-locality friendly copy of LogEnabled + ImGuiID ActiveId; // Active widget + ImGuiID ActiveIdIsAlive; // Active widget has been seen this frame (we can't use a bool as the ActiveId may change within the frame) + float ActiveIdTimer; + bool ActiveIdIsJustActivated; // Set at the time of activation for one frame + bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + bool ActiveIdNoClearOnFocusLoss; // Disable losing active id if the active id window gets unfocused. + bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. + bool ActiveIdHasBeenEditedBefore; // Was the value associated to the widget Edited over the course of the Active state. + bool ActiveIdHasBeenEditedThisFrame; + bool ActiveIdFromShortcut; + int ActiveIdMouseButton : 8; + ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) + ImGuiWindow* ActiveIdWindow; + ImGuiInputSource ActiveIdSource; // Activating source: ImGuiInputSource_Mouse OR ImGuiInputSource_Keyboard OR ImGuiInputSource_Gamepad + ImGuiID ActiveIdPreviousFrame; + bool ActiveIdPreviousFrameIsAlive; + bool ActiveIdPreviousFrameHasBeenEditedBefore; + ImGuiWindow* ActiveIdPreviousFrameWindow; + ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. + float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + + // Key/Input Ownership + Shortcut Routing system + // - The idea is that instead of "eating" a given key, we can link to an owner. + // - Input query can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Routing is requested ahead of time for a given chord (Key + Mods) and granted in NewFrame(). + double LastKeyModsChangeTime; // Record the last time key mods changed (affect repeat delay when using shortcut logic) + double LastKeyModsChangeFromNoneTime; // Record the last time key mods changed away from being 0 (affect repeat delay when using shortcut logic) + double LastKeyboardKeyPressTime; // Record the last time a keyboard key (ignore mouse/gamepad ones) was pressed. + ImBitArrayForNamedKeys KeysMayBeCharInput; // Lookup to tell if a key can emit char input, see IsKeyChordPotentiallyCharInput(). sizeof() = 20 bytes + ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; + ImGuiKeyRoutingTable KeysRoutingTable; + ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) + bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (this is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations) + ImGuiKeyChord DebugBreakInShortcutRouting; // Set to break in SetShortcutRouting()/Shortcut() calls. + //ImU32 ActiveIdUsingNavInputMask; // [OBSOLETE] Since (IMGUI_VERSION_NUM >= 18804) : 'g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);' becomes --> 'SetKeyOwner(ImGuiKey_Escape, g.ActiveId) and/or SetKeyOwner(ImGuiKey_NavGamepadCancel, g.ActiveId);' + + // Next window/item data + ImGuiID CurrentFocusScopeId; // Value for currently appending items == g.FocusScopeStack.back(). Not to be mistaken with g.NavFocusScopeId. + ImGuiItemFlags CurrentItemFlags; // Value for currently appending items == g.ItemFlagsStack.back() + ImGuiID DebugLocateId; // Storage for DebugLocateItemOnHover() feature: this is read by ItemAdd() so we keep it in a hot/cached location + ImGuiNextItemData NextItemData; // Storage for SetNextItem** functions + ImGuiLastItemData LastItemData; // Storage for last submitted item (setup by ItemAdd) + ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + bool DebugShowGroupRects; + + // Shared stacks + ImGuiCol DebugFlashStyleColorIdx; // (Keep close to ColorStack to share cache line) + ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() + ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() + ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() + ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin() + ImVector ItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() + ImVector GroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() + ImVector OpenPopupStack; // Which popups are open (persistent) + ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImVectorTreeNodeStack; // Stack for TreeNode() + + // Viewports + ImVector Viewports; // Active viewports (Size==1 in 'master' branch). Each viewports hold their copy of ImDrawData. + + // Gamepad/keyboard Navigation + ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusedWindow' + ImGuiID NavId; // Focused item for navigation + ImGuiID NavFocusScopeId; // Focused focus scope (e.g. selection code often wants to "clear other items" when landing on an item of the same scope) + ImGuiNavLayer NavLayer; // Focused layer (main scrolling layer, or menu/title bar layer) + ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItem() + ImGuiID NavActivateDownId; // ~~ IsKeyDown(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyDown(ImGuiKey_NavGamepadActivate) ? NavId : 0 + ImGuiID NavActivatePressedId; // ~~ IsKeyPressed(ImGuiKey_Space) || IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate) ? NavId : 0 (no repeat) + ImGuiActivateFlags NavActivateFlags; + ImVector NavFocusRoute; // Reversed copy focus scope stack for NavId (should contains NavFocusScopeId). This essentially follow the window->ParentWindowForFocusRoute chain. + ImGuiID NavHighlightActivatedId; + float NavHighlightActivatedTimer; + ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame. + ImGuiActivateFlags NavNextActivateFlags; + ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Mouse + ImGuiSelectionUserData NavLastValidSelectionUserData; // Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data. + bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRectRel is valid + bool NavMousePosDirty; // When set we will update mouse position if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) if set (NB: this not enabled by default) + bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (NB: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) + bool NavDisableMouseHover; // When user starts using gamepad/keyboard, we hide mouse hovering highlight until mouse is touched again. + + // Navigation: Init & Move Requests + bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest this is to perform early out in ItemAdd() + bool NavInitRequest; // Init request for appearing window to select first item + bool NavInitRequestFromMove; + ImGuiNavItemData NavInitResult; // Init request result (first item of the window, or one for which SetItemDefaultFocus() was called) + bool NavMoveSubmitted; // Move request submitted, will process result on next NewFrame() + bool NavMoveScoringItems; // Move request submitted, still scoring incoming items + bool NavMoveForwardToNextFrame; + ImGuiNavMoveFlags NavMoveFlags; + ImGuiScrollFlags NavMoveScrollFlags; + ImGuiKeyChord NavMoveKeyMods; + ImGuiDir NavMoveDir; // Direction of the move request (left/right/up/down) + ImGuiDir NavMoveDirForDebug; + ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? + ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring. + ImRect NavScoringNoClipRect; // Some nav operations (such as PageUp/PageDown) enforce a region which clipper will attempt to always keep submitted + int NavScoringDebugCount; // Metrics for debugging + int NavTabbingDir; // Generally -1 or +1, 0 when tabbing without a nav id + int NavTabbingCounter; // >0 when counting items for tabbing + ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow + ImGuiNavItemData NavMoveResultLocalVisible; // Best move request candidate within NavWindow that are mostly visible (when using ImGuiNavMoveFlags_AlsoScoreVisibleSet flag) + ImGuiNavItemData NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using ImGuiWindowFlags_NavFlattened flag) + ImGuiNavItemData NavTabbingResultFirst; // First tabbing request candidate within NavWindow and flattened hierarchy + + // Navigation: record of last move request + ImGuiID NavJustMovedFromFocusScopeId; // Just navigated from this focus scope id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToFocusScopeId; // Just navigated to this focus scope id (result of a successfully MoveRequest). + ImGuiKeyChord NavJustMovedToKeyMods; + bool NavJustMovedToIsTabbing; // Copy of ImGuiNavMoveFlags_IsTabbing. Maybe we should store whole flags. + bool NavJustMovedToHasSelectionData; // Copy of move result's InFlags & ImGuiItemFlags_HasSelectionUserData). Maybe we should just store ImGuiNavItemData. + + // Navigation: Windowing (CTRL+TAB for list, or Menu button + keys or directional pads to move/resize) + ImGuiKeyChord ConfigNavWindowingKeyNext; // = ImGuiMod_Ctrl | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiKey_Tab on OS X). For reconfiguration (see #4828) + ImGuiKeyChord ConfigNavWindowingKeyPrev; // = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab on OS X) + ImGuiWindow* NavWindowingTarget; // Target window when doing CTRL+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most! + ImGuiWindow* NavWindowingTargetAnim; // Record of last valid NavWindowingTarget until DimBgRatio and NavWindowingHighlightAlpha becomes 0.0f, so the fade-out can stay on it. + ImGuiWindow* NavWindowingListWindow; // Internal window actually listing the CTRL+Tab contents + float NavWindowingTimer; + float NavWindowingHighlightAlpha; + bool NavWindowingToggleLayer; + ImGuiKey NavWindowingToggleKey; + ImVec2 NavWindowingAccumDeltaPos; + ImVec2 NavWindowingAccumDeltaSize; + + // Render + float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) + + // Drag and Drop + bool DragDropActive; + bool DragDropWithinSource; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag source. + bool DragDropWithinTarget; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag target. + ImGuiDragDropFlags DragDropSourceFlags; + int DragDropSourceFrameCount; + int DragDropMouseButton; + ImGuiPayload DragDropPayload; + ImRect DragDropTargetRect; // Store rectangle of current target candidate (we favor small targets when overlapping) + ImRect DragDropTargetClipRect; // Store ClipRect at the time of item's drawing + ImGuiID DragDropTargetId; + ImGuiDragDropFlags DragDropAcceptFlags; + float DragDropAcceptIdCurrRectSurface; // Target item surface (we resolve overlapping targets by prioritizing the smaller surface) + ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) + ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) + int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source + ImGuiID DragDropHoldJustPressedId; // Set when holding a payload just made ButtonBehavior() return a press. + ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size + unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads + + // Clipper + int ClipperTempDataStacked; + ImVector ClipperTempData; + + // Tables + ImGuiTable* CurrentTable; + ImGuiID DebugBreakInTable; // Set to break in BeginTable() call. + int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) + ImVector TablesTempData; // Temporary table data (buffers reused/shared across instances, support nesting) + ImPool Tables; // Persistent table data + ImVector TablesLastTimeActive; // Last used timestamp of each tables (SOA, for efficient GC) + ImVector DrawChannelsTempMergeBuffer; + + // Tab bars + ImGuiTabBar* CurrentTabBar; + ImPool TabBars; + ImVector CurrentTabBarStack; + ImVector ShrinkWidthBuffer; + + // Multi-Select state + ImGuiBoxSelectState BoxSelectState; + ImGuiMultiSelectTempData* CurrentMultiSelect; + int MultiSelectTempDataStacked; // Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size) + ImVector MultiSelectTempData; + ImPool MultiSelectStorage; + + // Hover Delay system + ImGuiID HoverItemDelayId; + ImGuiID HoverItemDelayIdPreviousFrame; + float HoverItemDelayTimer; // Currently used by IsItemHovered() + float HoverItemDelayClearTimer; // Currently used by IsItemHovered(): grace time before g.TooltipHoverTimer gets cleared. + ImGuiID HoverItemUnlockedStationaryId; // Mouse has once been stationary on this item. Only reset after departing the item. + ImGuiID HoverWindowUnlockedStationaryId; // Mouse has once been stationary on this window. Only reset after departing the window. + + // Mouse state + ImGuiMouseCursor MouseCursor; + float MouseStationaryTimer; // Time the mouse has been stationary (with some loose heuristic) + ImVec2 MouseLastValidPos; + + // Widget state + ImGuiInputTextState InputTextState; + ImGuiInputTextDeactivatedState InputTextDeactivatedState; + ImFont InputTextPasswordFont; + ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiDataTypeStorage DataTypeZeroValue; // 0 for all data types + int BeginMenuDepth; + int BeginComboDepth; + ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets + ImGuiID ColorEditCurrentID; // Set temporarily while inside of the parent-most ColorEdit4/ColorPicker4 (because they call each others). + ImGuiID ColorEditSavedID; // ID we are saving/restoring HS for + float ColorEditSavedHue; // Backup of last Hue associated to LastColor, so we can restore Hue in lossy RGB<>HSV round trips + float ColorEditSavedSat; // Backup of last Saturation associated to LastColor, so we can restore Saturation in lossy RGB<>HSV round trips + ImU32 ColorEditSavedColor; // RGB value with alpha set to 0. + ImVec4 ColorPickerRef; // Initial/reference color at the time of opening the color picker. + ImGuiComboPreviewData ComboPreviewData; + ImRect WindowResizeBorderExpectedRect; // Expected border rect, switch to relative edit if moving + bool WindowResizeRelativeMode; + short ScrollbarSeekMode; // 0: relative, -1/+1: prev/next page. + float ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? + float SliderGrabClickOffset; + float SliderCurrentAccum; // Accumulated slider delta when using navigation controls. + bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it? + bool DragCurrentAccumDirty; + float DragCurrentAccum; // Accumulator for dragging modification. Always high-precision, not rounded by end-user precision settings + float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio + float DisabledAlphaBackup; // Backup for style.Alpha for BeginDisabled() + short DisabledStackSize; + short LockMarkEdited; + short TooltipOverrideCount; + ImVector ClipboardHandlerData; // If no custom clipboard handler is defined + ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once + ImGuiTypingSelectState TypingSelectState; // State for GetTypingSelectRequest() + + // Platform support + ImGuiPlatformImeData PlatformImeData; // Data updated by current frame + ImGuiPlatformImeData PlatformImeDataPrev; // Previous frame data. When changed we call the platform_io.Platform_SetImeDataFn() handler. + + // Settings + bool SettingsLoaded; + float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero + ImGuiTextBuffer SettingsIniData; // In memory .ini settings + ImVector SettingsHandlers; // List of .ini settings handlers + ImChunkStream SettingsWindows; // ImGuiWindow .ini settings entries + ImChunkStream SettingsTables; // ImGuiTable .ini settings entries + ImVector Hooks; // Hooks for extensions (e.g. test engine) + ImGuiID HookIdNext; // Next available HookId + + // Localization + const char* LocalizationTable[ImGuiLocKey_COUNT]; + + // Capture/Logging + bool LogEnabled; // Currently capturing + ImGuiLogType LogType; // Capture target + ImFileHandle LogFile; // If != NULL log to stdout/ file + ImGuiTextBuffer LogBuffer; // Accumulation buffer when log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. + const char* LogNextPrefix; + const char* LogNextSuffix; + float LogLinePosY; + bool LogLineFirstItem; + int LogDepthRef; + int LogDepthToExpand; + int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. + + // Debug Tools + // (some of the highly frequently used data are interleaved in other structures above: DebugBreakXXX fields, DebugHookIdInfo, DebugLocateId etc.) + ImGuiDebugLogFlags DebugLogFlags; + ImGuiTextBuffer DebugLogBuf; + ImGuiTextIndex DebugLogIndex; + ImGuiDebugLogFlags DebugLogAutoDisableFlags; + ImU8 DebugLogAutoDisableFrames; + ImU8 DebugLocateFrames; // For DebugLocateItemOnHover(). This is used together with DebugLocateId which is in a hot/cached spot above. + bool DebugBreakInLocateId; // Debug break in ItemAdd() call for g.DebugLocateId. + ImGuiKeyChord DebugBreakKeyChord; // = ImGuiKey_Pause + ImS8 DebugBeginReturnValueCullDepth; // Cycle between 0..9 then wrap around. + bool DebugItemPickerActive; // Item picker is active (started with DebugStartItemPicker()) + ImU8 DebugItemPickerMouseButton; + ImGuiID DebugItemPickerBreakId; // Will call IM_DEBUG_BREAK() when encountering this ID + float DebugFlashStyleColorTime; + ImVec4 DebugFlashStyleColorBackup; + ImGuiMetricsConfig DebugMetricsConfig; + ImGuiIDStackTool DebugIDStackTool; + ImGuiDebugAllocInfo DebugAllocInfo; + + // Misc + float FramerateSecPerFrame[60]; // Calculate estimate of framerate for user over the last 60 frames.. + int FramerateSecPerFrameIdx; + int FramerateSecPerFrameCount; + float FramerateSecPerFrameAccum; + int WantCaptureMouseNextFrame; // Explicit capture override via SetNextFrameWantCaptureMouse()/SetNextFrameWantCaptureKeyboard(). Default to -1. + int WantCaptureKeyboardNextFrame; // " + int WantTextInputNextFrame; + ImVector TempBuffer; // Temporary text buffer + char TempKeychordName[64]; + + ImGuiContext(ImFontAtlas* shared_font_atlas) + { + IO.Ctx = this; + InputTextState.Ctx = this; + + Initialized = false; + FontAtlasOwnedByContext = shared_font_atlas ? false : true; + Font = NULL; + FontSize = FontBaseSize = FontScale = CurrentDpiScale = 0.0f; + IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); + Time = 0.0f; + FrameCount = 0; + FrameCountEnded = FrameCountRendered = -1; + WithinFrameScope = WithinFrameScopeWithImplicitWindow = WithinEndChild = false; + GcCompactAll = false; + TestEngineHookItems = false; + TestEngine = NULL; + memset(ContextName, 0, sizeof(ContextName)); + + InputEventsNextMouseSource = ImGuiMouseSource_Mouse; + InputEventsNextEventId = 1; + + WindowsActiveCount = 0; + CurrentWindow = NULL; + HoveredWindow = NULL; + HoveredWindowUnderMovingWindow = NULL; + HoveredWindowBeforeClear = NULL; + MovingWindow = NULL; + WheelingWindow = NULL; + WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; + WheelingWindowReleaseTimer = 0.0f; + + DebugHookIdInfo = 0; + HoveredId = HoveredIdPreviousFrame = 0; + HoveredIdAllowOverlap = false; + HoveredIdIsDisabled = false; + HoveredIdTimer = HoveredIdNotActiveTimer = 0.0f; + ItemUnclipByLog = false; + ActiveId = 0; + ActiveIdIsAlive = 0; + ActiveIdTimer = 0.0f; + ActiveIdIsJustActivated = false; + ActiveIdAllowOverlap = false; + ActiveIdNoClearOnFocusLoss = false; + ActiveIdHasBeenPressedBefore = false; + ActiveIdHasBeenEditedBefore = false; + ActiveIdHasBeenEditedThisFrame = false; + ActiveIdFromShortcut = false; + ActiveIdClickOffset = ImVec2(-1, -1); + ActiveIdWindow = NULL; + ActiveIdSource = ImGuiInputSource_None; + ActiveIdMouseButton = -1; + ActiveIdPreviousFrame = 0; + ActiveIdPreviousFrameIsAlive = false; + ActiveIdPreviousFrameHasBeenEditedBefore = false; + ActiveIdPreviousFrameWindow = NULL; + LastActiveId = 0; + LastActiveIdTimer = 0.0f; + + LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; + + ActiveIdUsingNavDirMask = 0x00; + ActiveIdUsingAllKeyboardKeys = false; + + CurrentFocusScopeId = 0; + CurrentItemFlags = ImGuiItemFlags_None; + DebugShowGroupRects = false; + + NavWindow = NULL; + NavId = NavFocusScopeId = NavActivateId = NavActivateDownId = NavActivatePressedId = 0; + NavLayer = ImGuiNavLayer_Main; + NavNextActivateId = 0; + NavActivateFlags = NavNextActivateFlags = ImGuiActivateFlags_None; + NavHighlightActivatedId = 0; + NavHighlightActivatedTimer = 0.0f; + NavInputSource = ImGuiInputSource_Keyboard; + NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + NavIdIsAlive = false; + NavMousePosDirty = false; + NavDisableHighlight = true; + NavDisableMouseHover = false; + + NavAnyRequest = false; + NavInitRequest = false; + NavInitRequestFromMove = false; + NavMoveSubmitted = false; + NavMoveScoringItems = false; + NavMoveForwardToNextFrame = false; + NavMoveFlags = ImGuiNavMoveFlags_None; + NavMoveScrollFlags = ImGuiScrollFlags_None; + NavMoveKeyMods = ImGuiMod_None; + NavMoveDir = NavMoveDirForDebug = NavMoveClipDir = ImGuiDir_None; + NavScoringDebugCount = 0; + NavTabbingDir = 0; + NavTabbingCounter = 0; + + NavJustMovedFromFocusScopeId = NavJustMovedToId = NavJustMovedToFocusScopeId = 0; + NavJustMovedToKeyMods = ImGuiMod_None; + NavJustMovedToIsTabbing = false; + NavJustMovedToHasSelectionData = false; + + // All platforms use Ctrl+Tab but Ctrl<>Super are swapped on Mac... + // FIXME: Because this value is stored, it annoyingly interfere with toggling io.ConfigMacOSXBehaviors updating this.. + ConfigNavWindowingKeyNext = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiKey_Tab); + ConfigNavWindowingKeyPrev = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab); + NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL; + NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; + NavWindowingToggleLayer = false; + NavWindowingToggleKey = ImGuiKey_None; + + DimBgRatio = 0.0f; + + DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; + DragDropSourceFlags = ImGuiDragDropFlags_None; + DragDropSourceFrameCount = -1; + DragDropMouseButton = -1; + DragDropTargetId = 0; + DragDropAcceptFlags = ImGuiDragDropFlags_None; + DragDropAcceptIdCurrRectSurface = 0.0f; + DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; + DragDropAcceptFrameCount = -1; + DragDropHoldJustPressedId = 0; + memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + + ClipperTempDataStacked = 0; + + CurrentTable = NULL; + TablesTempDataStacked = 0; + CurrentTabBar = NULL; + CurrentMultiSelect = NULL; + MultiSelectTempDataStacked = 0; + + HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; + HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; + + MouseCursor = ImGuiMouseCursor_Arrow; + MouseStationaryTimer = 0.0f; + + TempInputId = 0; + memset(&DataTypeZeroValue, 0, sizeof(DataTypeZeroValue)); + BeginMenuDepth = BeginComboDepth = 0; + ColorEditOptions = ImGuiColorEditFlags_DefaultOptions_; + ColorEditCurrentID = ColorEditSavedID = 0; + ColorEditSavedHue = ColorEditSavedSat = 0.0f; + ColorEditSavedColor = 0; + WindowResizeRelativeMode = false; + ScrollbarSeekMode = 0; + ScrollbarClickDeltaToGrabCenter = 0.0f; + SliderGrabClickOffset = 0.0f; + SliderCurrentAccum = 0.0f; + SliderCurrentAccumDirty = false; + DragCurrentAccumDirty = false; + DragCurrentAccum = 0.0f; + DragSpeedDefaultRatio = 1.0f / 100.0f; + DisabledAlphaBackup = 0.0f; + DisabledStackSize = 0; + LockMarkEdited = 0; + TooltipOverrideCount = 0; + + PlatformImeData.InputPos = ImVec2(0.0f, 0.0f); + PlatformImeDataPrev.InputPos = ImVec2(-1.0f, -1.0f); // Different to ensure initial submission + + SettingsLoaded = false; + SettingsDirtyTimer = 0.0f; + HookIdNext = 0; + + memset(LocalizationTable, 0, sizeof(LocalizationTable)); + + LogEnabled = false; + LogType = ImGuiLogType_None; + LogNextPrefix = LogNextSuffix = NULL; + LogFile = NULL; + LogLinePosY = FLT_MAX; + LogLineFirstItem = false; + LogDepthRef = 0; + LogDepthToExpand = LogDepthToExpandDefault = 2; + + DebugLogFlags = ImGuiDebugLogFlags_OutputToTTY; + DebugLocateId = 0; + DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + DebugLogAutoDisableFrames = 0; + DebugLocateFrames = 0; + DebugBeginReturnValueCullDepth = -1; + DebugItemPickerActive = false; + DebugItemPickerMouseButton = ImGuiMouseButton_Left; + DebugItemPickerBreakId = 0; + DebugFlashStyleColorTime = 0.0f; + DebugFlashStyleColorIdx = ImGuiCol_COUNT; + + // Same as DebugBreakClearData(). Those fields are scattered in their respective subsystem to stay in hot-data locations + DebugBreakInWindow = 0; + DebugBreakInTable = 0; + DebugBreakInLocateId = false; + DebugBreakKeyChord = ImGuiKey_Pause; + DebugBreakInShortcutRouting = ImGuiKey_None; + + memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); + FramerateSecPerFrameIdx = FramerateSecPerFrameCount = 0; + FramerateSecPerFrameAccum = 0.0f; + WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempKeychordName, 0, sizeof(TempKeychordName)); + } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiWindowTempData, ImGuiWindow +//----------------------------------------------------------------------------- + +// Transient per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the DC variable name in ImGuiWindow. +// (That's theory, in practice the delimitation between ImGuiWindow and ImGuiWindowTempData is quite tenuous and could be reconsidered..) +// (This doesn't need a constructor because we zero-clear it as part of ImGuiWindow and all frame-temporary data are setup on Begin) +struct IMGUI_API ImGuiWindowTempData +{ + // Layout + ImVec2 CursorPos; // Current emitting position, in absolute coordinates. + ImVec2 CursorPosPrevLine; + ImVec2 CursorStartPos; // Initial position after Begin(), generally ~ window position + WindowPadding. + ImVec2 CursorMaxPos; // Used to implicitly calculate ContentSize at the beginning of next frame, for scrolling range and auto-resize. Always growing during the frame. + ImVec2 IdealMaxPos; // Used to implicitly calculate ContentSizeIdeal at the beginning of next frame, for auto-resize only. Always growing during the frame. + ImVec2 CurrLineSize; + ImVec2 PrevLineSize; + float CurrLineTextBaseOffset; // Baseline offset (0.0f by default on a new line, generally == style.FramePadding.y when a framed item has been added). + float PrevLineTextBaseOffset; + bool IsSameLine; + bool IsSetPos; + ImVec1 Indent; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) + ImVec1 ColumnsOffset; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. + ImVec1 GroupOffset; + ImVec2 CursorStartPosLossyness;// Record the loss of precision of CursorStartPos due to really large scrolling amount. This is used by clipper to compensate and fix the most common use case of large scroll area. + + // Keyboard/Gamepad navigation + ImGuiNavLayer NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) + short NavLayersActiveMask; // Which layers have been written to (result from previous frame) + short NavLayersActiveMaskNext;// Which layers have been written to (accumulator for current frame) + bool NavIsScrollPushableX; // Set when current work location may be scrolled horizontally when moving left / right. This is generally always true UNLESS within a column. + bool NavHideHighlightOneFrame; + bool NavWindowHasScrollY; // Set per window when scrolling can be used (== ScrollMax.y > 0.0f) + + // Miscellaneous + bool MenuBarAppending; // FIXME: Remove this + ImVec2 MenuBarOffset; // MenuBarOffset.x is sort of equivalent of a per-layer CursorPos.x, saved/restored as we switch to the menu bar. The only situation when MenuBarOffset.y is > 0 if when (SafeAreaPadding.y > FramePadding.y), often used on TVs. + ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items measurement + int TreeDepth; // Current tree depth. + ImU32 TreeHasStackDataDepthMask; // Store whether given depth has ImGuiTreeNodeStackData data. Could be turned into a ImU64 if necessary. + ImVector ChildWindows; + ImGuiStorage* StateStorage; // Current persistent per-window storage (store e.g. tree node open/close state) + ImGuiOldColumns* CurrentColumns; // Current columns set + int CurrentTableIdx; // Current table index (into g.Tables) + ImGuiLayoutType LayoutType; + ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + ImU32 ModalDimBgColor; + + // Local parameters stacks + // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. + float ItemWidth; // Current item width (>0.0: width in pixels, <0.0: align xx pixels to the right of window). + float TextWrapPos; // Current text wrap pos. + ImVector ItemWidthStack; // Store item widths to restore (attention: .back() is not == ItemWidth) + ImVector TextWrapPosStack; // Store text wrap pos to restore (attention: .back() is not == TextWrapPos) +}; + +// Storage for one window struct IMGUI_API ImGuiWindow { - char *Name; - ImGuiID ID; // == ImHash(Name) - ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ - ImVec2 PosFloat; - ImVec2 Pos; // Position rounded-up to nearest pixel - ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) - ImVec2 SizeFull; // Size when non collapsed - ImVec2 SizeFullAtLastBegin; // Copy of SizeFull at the end of Begin. This is the reference value we'll use on the next frame to decide if we need scrollbars. - ImVec2 SizeContents; // Size of contents (== extents reach of the drawing cursor) from previous frame. Include decoration, window title, border, menu, etc. - ImVec2 SizeContentsExplicit; // Size of contents explicitly set by the user via SetNextWindowContentSize() - ImRect ContentsRegionRect; // Maximum visible content position in window coordinates. ~~ (SizeContentsExplicit ? SizeContentsExplicit : Size - ScrollbarSizes) - CursorStartPos, per axis - ImVec2 WindowPadding; // Window padding at the time of begin. - float WindowRounding; // Window rounding at the time of begin. - float WindowBorderSize; // Window border size at the time of begin. - ImGuiID MoveId; // == window->GetID("#MOVE") - ImGuiID ChildId; // Id of corresponding item in parent window (for child windows) - ImVec2 Scroll; - ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) - ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered - bool ScrollbarX, ScrollbarY; - ImVec2 ScrollbarSizes; - bool Active; // Set to true on Begin(), unless Collapsed - bool WasActive; - bool WriteAccessed; // Set to true when any widget access the current window - bool Collapsed; // Set when collapsing window to become only title-bar - bool CollapseToggleWanted; - bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) - bool Appearing; // Set during the frame where the window is appearing (or re-appearing) - bool CloseButton; // Set when the window has a close button (p_open != NULL) - int BeginOrderWithinParent; // Order within immediate parent window, if we are a child window. Otherwise 0. - int BeginOrderWithinContext; // Order within entire imgui context. This is mostly used for debugging submission order related issues. - int BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) - ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) - int AutoFitFramesX, AutoFitFramesY; - bool AutoFitOnlyGrows; - int AutoFitChildAxises; - ImGuiDir AutoPosLastDirection; - int HiddenFrames; - ImGuiCond SetWindowPosAllowFlags; // store condition flags for next SetWindowPos() call. - ImGuiCond SetWindowSizeAllowFlags; // store condition flags for next SetWindowSize() call. - ImGuiCond SetWindowCollapsedAllowFlags; // store condition flags for next SetWindowCollapsed() call. - ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) - ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0,0) when positioning from top-left corner; ImVec2(0.5f,0.5f) for centering; ImVec2(1,1) for bottom right. - - ImGuiDrawContext DC; // Temporary per-window data, reset at the beginning of the frame - ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack - ImRect ClipRect; // = DrawList->clip_rect_stack.back(). Scissoring / clipping rectangle. x1, y1, x2, y2. - ImRect WindowRectClipped; // = WindowRect just after setup in Begin(). == window->Rect() for root window. - ImRect InnerRect; - int LastFrameActive; - float ItemWidthDefault; - ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items - ImGuiStorage StateStorage; - ImVector ColumnsStorage; - float FontWindowScale; // Scale multiplier per-window - ImDrawList *DrawList; - ImGuiWindow *ParentWindow; // If we are a child _or_ popup window, this is pointing to our parent. Otherwise NULL. - ImGuiWindow *RootWindow; // Point to ourself or first ancestor that is not a child window. - ImGuiWindow *RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. - ImGuiWindow *RootWindowForTabbing; // Point to ourself or first ancestor which can be CTRL-Tabbed into. - ImGuiWindow *RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. - - ImGuiWindow *NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) - ImGuiID NavLastIds[2]; // Last known NavId for this window, per layer (0/1) - ImRect NavRectRel[2]; // Reference rectangle, in window relative space - - // Navigation / Focus - // FIXME-NAV: Merge all this with the new Nav system, at least the request variables should be moved to ImGuiContext - int FocusIdxAllCounter; // Start at -1 and increase as assigned via FocusItemRegister() - int FocusIdxTabCounter; // (same, but only count widgets which you can Tab through) - int FocusIdxAllRequestCurrent; // Item being requested for focus - int FocusIdxTabRequestCurrent; // Tab-able item being requested for focus - int FocusIdxAllRequestNext; // Item being requested for focus, for next update (relies on layout to be stable between the frame pressing TAB and the next frame) - int FocusIdxTabRequestNext; // " - - public: - ImGuiWindow(ImGuiContext *context, const char *name); - ~ImGuiWindow(); - - ImGuiID GetID(const char *str, const char *str_end = NULL); - ImGuiID GetID(const void *ptr); - ImGuiID GetIDNoKeepAlive(const char *str, const char *str_end = NULL); - ImGuiID GetIDFromRectangle(const ImRect &r_abs); - - // We don't use g.FontSize because the window may be != g.CurrentWidow. - ImRect Rect() const - { - return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); - } - float CalcFontSize() const - { - return GImGui->FontBaseSize * FontWindowScale; - } - float TitleBarHeight() const - { - return (Flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : CalcFontSize() + GImGui->Style.FramePadding.y * 2.0f; - } - ImRect TitleBarRect() const - { - return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight())); - } - float MenuBarHeight() const - { - return (Flags & ImGuiWindowFlags_MenuBar) ? CalcFontSize() + GImGui->Style.FramePadding.y * 2.0f : 0.0f; - } - ImRect MenuBarRect() const - { - float y1 = Pos.y + TitleBarHeight(); - return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight()); - } -}; - -// Backup and restore just enough data to be able to use IsItemHovered() on item A after another B in the same window has overwritten the data. -struct ImGuiItemHoveredDataBackup -{ - ImGuiID LastItemId; - ImGuiItemStatusFlags LastItemStatusFlags; - ImRect LastItemRect; - ImRect LastItemDisplayRect; - - ImGuiItemHoveredDataBackup() - { - Backup(); - } - void Backup() - { - ImGuiWindow *window = GImGui->CurrentWindow; - LastItemId = window->DC.LastItemId; - LastItemStatusFlags = window->DC.LastItemStatusFlags; - LastItemRect = window->DC.LastItemRect; - LastItemDisplayRect = window->DC.LastItemDisplayRect; - } - void Restore() const - { - ImGuiWindow *window = GImGui->CurrentWindow; - window->DC.LastItemId = LastItemId; - window->DC.LastItemStatusFlags = LastItemStatusFlags; - window->DC.LastItemRect = LastItemRect; - window->DC.LastItemDisplayRect = LastItemDisplayRect; - } + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + char* Name; // Window name, owned by the window. + ImGuiID ID; // == ImHashStr(Name) + ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImGuiChildFlags ChildFlags; // Set when window is a child window. See enum ImGuiChildFlags_ + ImGuiViewportP* Viewport; // Always set in Begin(). Inactive windows may have a NULL value here if their viewport was discarded. + ImVec2 Pos; // Position (always rounded-up to nearest pixel) + ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) + ImVec2 SizeFull; // Size when non collapsed + ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding. + ImVec2 ContentSizeIdeal; + ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize(). + ImVec2 WindowPadding; // Window padding at the time of Begin(). + float WindowRounding; // Window rounding at the time of Begin(). May be clamped lower to avoid rendering artifacts with title bar, menu bar etc. + float WindowBorderSize; // Window border size at the time of Begin(). + float TitleBarHeight, MenuBarHeight; // Note that those used to be function before 2024/05/28. If you have old code calling TitleBarHeight() you can change it to TitleBarHeight. + float DecoOuterSizeX1, DecoOuterSizeY1; // Left/Up offsets. Sum of non-scrolling outer decorations (X1 generally == 0.0f. Y1 generally = TitleBarHeight + MenuBarHeight). Locked during Begin(). + float DecoOuterSizeX2, DecoOuterSizeY2; // Right/Down offsets (X2 generally == ScrollbarSize.x, Y2 == ScrollbarSizes.y). + float DecoInnerSizeX1, DecoInnerSizeY1; // Applied AFTER/OVER InnerRect. Specialized for Tables as they use specialized form of clipping and frozen rows/columns are inside InnerRect (and not part of regular decoration sizes). + int NameBufLen; // Size of buffer storing Name. May be larger than strlen(Name)! + ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID ChildId; // ID of corresponding item in parent window (for navigation to return from child window to parent window) + ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) + ImVec2 Scroll; + ImVec2 ScrollMax; + ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) + ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered + ImVec2 ScrollTargetEdgeSnapDist; // 0.0f = no snapping, >0.0f snapping threshold + ImVec2 ScrollbarSizes; // Size taken by each scrollbars on their smaller axis. Pay attention! ScrollbarSizes.x == width of the vertical scrollbar, ScrollbarSizes.y = height of the horizontal scrollbar. + bool ScrollbarX, ScrollbarY; // Are scrollbars visible? + bool Active; // Set to true on Begin(), unless Collapsed + bool WasActive; + bool WriteAccessed; // Set to true when any widget access the current window + bool Collapsed; // Set when collapsing window to become only title-bar + bool WantCollapseToggle; + bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool SkipRefresh; // [EXPERIMENTAL] Reuse previous frame drawn contents, Begin() returns false. + bool Appearing; // Set during the frame where the window is appearing (or re-appearing) + bool Hidden; // Do not display (== HiddenFrames*** > 0) + bool IsFallbackWindow; // Set on the "Debug##Default" window. + bool IsExplicitChild; // Set when passed _ChildWindow, left to false by BeginDocked() + bool HasCloseButton; // Set when the window has a close button (p_open != NULL) + signed char ResizeBorderHovered; // Current border being hovered for resize (-1: none, otherwise 0-3) + signed char ResizeBorderHeld; // Current border being held for resize (-1: none, otherwise 0-3) + short BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) + short BeginCountPreviousFrame; // Number of Begin() during the previous frame + short BeginOrderWithinParent; // Begin() order within immediate parent window, if we are a child window. Otherwise 0. + short BeginOrderWithinContext; // Begin() order within entire imgui context. This is mostly used for debugging submission order related issues. + short FocusOrder; // Order within WindowsFocusOrder[], altered when windows are focused. + ImS8 AutoFitFramesX, AutoFitFramesY; + bool AutoFitOnlyGrows; + ImGuiDir AutoPosLastDirection; + ImS8 HiddenFramesCanSkipItems; // Hide the window for N frames + ImS8 HiddenFramesCannotSkipItems; // Hide the window for N frames while allowing items to be submitted so we can measure their size + ImS8 HiddenFramesForRenderOnly; // Hide the window until frame N at Render() time only + ImS8 DisableInputsFrames; // Disable window interactions for N frames + ImGuiCond SetWindowPosAllowFlags : 8; // store acceptable condition flags for SetNextWindowPos() use. + ImGuiCond SetWindowSizeAllowFlags : 8; // store acceptable condition flags for SetNextWindowSize() use. + ImGuiCond SetWindowCollapsedAllowFlags : 8; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) + ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0, 0) when positioning from top-left corner; ImVec2(0.5f, 0.5f) for centering; ImVec2(1, 1) for bottom right. + + ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack. (In theory this should be in the TempData structure) + ImGuiWindowTempData DC; // Temporary per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the "DC" variable name. + + // The best way to understand what those rectangles are is to use the 'Metrics->Tools->Show Windows Rectangles' viewer. + // The main 'OuterRect', omitted as a field, is window->Rect(). + ImRect OuterRectClipped; // == Window->Rect() just after setup in Begin(). == window->Rect() for root window. + ImRect InnerRect; // Inner rectangle (omit title bar, menu bar, scroll bar) + ImRect InnerClipRect; // == InnerRect shrunk by WindowPadding*0.5f on each side, clipped within viewport or parent clip rect. + ImRect WorkRect; // Initially covers the whole scrolling region. Reduced by containers e.g columns/tables when active. Shrunk by WindowPadding*1.0f on each side. This is meant to replace ContentRegionRect over time (from 1.71+ onward). + ImRect ParentWorkRect; // Backup of WorkRect before entering a container such as columns/tables. Used by e.g. SpanAllColumns functions to easily access. Stacked containers are responsible for maintaining this. // FIXME-WORKRECT: Could be a stack? + ImRect ClipRect; // Current clipping/scissoring rectangle, evolve as we are using PushClipRect(), etc. == DrawList->clip_rect_stack.back(). + ImRect ContentRegionRect; // FIXME: This is currently confusing/misleading. It is essentially WorkRect but not handling of scrolling. We currently rely on it as right/bottom aligned sizing operation need some size to rely on. + ImVec2ih HitTestHoleSize; // Define an optional rectangular hole where mouse will pass-through the window. + ImVec2ih HitTestHoleOffset; + + int LastFrameActive; // Last frame number the window was Active. + float LastTimeActive; // Last timestamp the window was Active (using float as we don't need high precision there) + float ItemWidthDefault; + ImGuiStorage StateStorage; + ImVector ColumnsStorage; + float FontWindowScale; // User scale multiplier per-window, via SetWindowFontScale() + int SettingsOffset; // Offset into SettingsWindows[] (offsets are always valid as we only grow the array from the back) + + ImDrawList* DrawList; // == &DrawListInst (for backward compatibility reason with code using imgui_internal.h we keep this a pointer) + ImDrawList DrawListInst; + ImGuiWindow* ParentWindow; // If we are a child _or_ popup _or_ docked window, this is pointing to our parent. Otherwise NULL. + ImGuiWindow* ParentWindowInBeginStack; + ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. Doesn't cross through popups/dock nodes. + ImGuiWindow* RootWindowPopupTree; // Point to ourself or first ancestor that is not a child window. Cross through popups parent<>child. + ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. + ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + ImGuiWindow* ParentWindowForFocusRoute; // Set to manual link a window to its logical parent so that Shortcut() chain are honoerd (e.g. Tool linked to Document) + + ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) + ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1) + ImRect NavRectRel[ImGuiNavLayer_COUNT]; // Reference rectangle, in window relative space + ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; // Preferred X/Y position updated when moving on a given axis, reset to FLT_MAX. + ImGuiID NavRootFocusScopeId; // Focus Scope ID at the time of Begin() + + int MemoryDrawListIdxCapacity; // Backup of last idx/vtx count, so when waking up the window we can preallocate and avoid iterative alloc/copy + int MemoryDrawListVtxCapacity; + bool MemoryCompacted; // Set when window extraneous data have been garbage collected + +public: + ImGuiWindow(ImGuiContext* context, const char* name); + ~ImGuiWindow(); + + ImGuiID GetID(const char* str, const char* str_end = NULL); + ImGuiID GetID(const void* ptr); + ImGuiID GetID(int n); + ImGuiID GetIDFromPos(const ImVec2& p_abs); + ImGuiID GetIDFromRectangle(const ImRect& r_abs); + + // We don't use g.FontSize because the window may be != g.CurrentWindow. + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + float CalcFontSize() const { ImGuiContext& g = *Ctx; float scale = g.FontBaseSize * FontWindowScale; if (ParentWindow) scale *= ParentWindow->FontWindowScale; return scale; } + ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight)); } + ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight; return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight); } }; //----------------------------------------------------------------------------- -// Internal API -// No guarantee of forward compatibility here. +// [SECTION] Tab bar, Tab item support //----------------------------------------------------------------------------- -namespace ImGui +// Extend ImGuiTabBarFlags_ +enum ImGuiTabBarFlagsPrivate_ { -// We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) -// If this ever crash because g.CurrentWindow is NULL it means that either -// - ImGui::NewFrame() has never been called, which is illegal. -// - You are calling ImGui functions after ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. -inline ImGuiWindow *GetCurrentWindowRead() + ImGuiTabBarFlags_DockNode = 1 << 20, // Part of a dock node [we don't use this in the master branch but it facilitate branch syncing to keep this around] + ImGuiTabBarFlags_IsFocused = 1 << 21, + ImGuiTabBarFlags_SaveSettings = 1 << 22, // FIXME: Settings are handled by the docking system, this only request the tab bar to mark settings dirty when reordering tabs +}; + +// Extend ImGuiTabItemFlags_ +enum ImGuiTabItemFlagsPrivate_ { - ImGuiContext &g = *GImGui; - return g.CurrentWindow; -} -inline ImGuiWindow *GetCurrentWindow() + ImGuiTabItemFlags_SectionMask_ = ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, + ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_Button = 1 << 21, // Used by TabItemButton, change the tab item behavior to mimic a button +}; + +// Storage for one active tab item (sizeof() 40 bytes) +struct ImGuiTabItem +{ + ImGuiID ID; + ImGuiTabItemFlags Flags; + int LastFrameVisible; + int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance + float Offset; // Position relative to beginning of tab + float Width; // Width currently displayed + float ContentWidth; // Width of label, stored during BeginTabItem() call + float RequestedWidth; // Width optionally requested by caller, -1.0f is unused + ImS32 NameOffset; // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames + ImS16 BeginOrder; // BeginTabItem() order, used to re-order tabs after toggling ImGuiTabBarFlags_Reorderable + ImS16 IndexDuringLayout; // Index only used during TabBarLayout(). Tabs gets reordered so 'Tabs[n].IndexDuringLayout == n' but may mismatch during additions. + bool WantClose; // Marked as closed by SetTabItemClosed() + + ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; } +}; + +// Storage for a tab bar (sizeof() 152 bytes) +struct IMGUI_API ImGuiTabBar +{ + ImVector Tabs; + ImGuiTabBarFlags Flags; + ImGuiID ID; // Zero for tab-bars used by docking + ImGuiID SelectedTabId; // Selected tab/window + ImGuiID NextSelectedTabId; // Next selected tab/window. Will also trigger a scrolling animation + ImGuiID VisibleTabId; // Can occasionally be != SelectedTabId (e.g. when previewing contents for CTRL+TAB preview) + int CurrFrameVisible; + int PrevFrameVisible; + ImRect BarRect; + float CurrTabsContentsHeight; + float PrevTabsContentsHeight; // Record the height of contents submitted below the tab bar + float WidthAllTabs; // Actual width of all tabs (locked during layout) + float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped + float ScrollingAnim; + float ScrollingTarget; + float ScrollingTargetDistToVisibility; + float ScrollingSpeed; + float ScrollingRectMinX; + float ScrollingRectMaxX; + float SeparatorMinX; + float SeparatorMaxX; + ImGuiID ReorderRequestTabId; + ImS16 ReorderRequestOffset; + ImS8 BeginCount; + bool WantLayout; + bool VisibleTabWasSubmitted; + bool TabsAddedNew; // Set to true when a new tab item or button has been added to the tab bar during last frame + ImS16 TabsActiveCount; // Number of tabs submitted this frame. + ImS16 LastTabItemIdx; // Index of last BeginTabItem() tab for use by EndTabItem() + float ItemSpacingY; + ImVec2 FramePadding; // style.FramePadding locked at the time of BeginTabBar() + ImVec2 BackupCursorPos; + ImGuiTextBuffer TabsNames; // For non-docking tab bar we re-append names in a contiguous buffer. + + ImGuiTabBar(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Table support +//----------------------------------------------------------------------------- + +#define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code which cannot be used as a regular color. +#define IMGUI_TABLE_MAX_COLUMNS 512 // May be further lifted + +// Our current column maximum is 64 but we may raise that in the future. +typedef ImS16 ImGuiTableColumnIdx; +typedef ImU16 ImGuiTableDrawChannelIdx; + +// [Internal] sizeof() ~ 112 +// We use the terminology "Enabled" to refer to a column that is not Hidden by user/api. +// We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping. +// This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped". +struct ImGuiTableColumn +{ + ImGuiTableColumnFlags Flags; // Flags after some patching (not directly same as provided by user). See ImGuiTableColumnFlags_ + float WidthGiven; // Final/actual width visible == (MaxX - MinX), locked in TableUpdateLayout(). May be > WidthRequest to honor minimum width, may be < WidthRequest to honor shrinking columns down in tight space. + float MinX; // Absolute positions + float MaxX; + float WidthRequest; // Master width absolute value when !(Flags & _WidthStretch). When Stretch this is derived every frame from StretchWeight in TableUpdateLayout() + float WidthAuto; // Automatic width + float WidthMax; // Maximum width (FIXME: overwritten by each instance) + float StretchWeight; // Master width weight when (Flags & _WidthStretch). Often around ~1.0f initially. + float InitStretchWeightOrWidth; // Value passed to TableSetupColumn(). For Width it is a content width (_without padding_). + ImRect ClipRect; // Clipping rectangle for the column + ImGuiID UserID; // Optional, value passed to TableSetupColumn() + float WorkMinX; // Contents region min ~(MinX + CellPaddingX + CellSpacingX1) == cursor start position when entering column + float WorkMaxX; // Contents region max ~(MaxX - CellPaddingX - CellSpacingX2) + float ItemWidth; // Current item width for the column, preserved across rows + float ContentMaxXFrozen; // Contents maximum position for frozen rows (apart from headers), from which we can infer content width. + float ContentMaxXUnfrozen; + float ContentMaxXHeadersUsed; // Contents maximum position for headers rows (regardless of freezing). TableHeader() automatically softclip itself + report ideal desired size, to avoid creating extraneous draw calls + float ContentMaxXHeadersIdeal; + ImS16 NameOffset; // Offset into parent ColumnsNames[] + ImGuiTableColumnIdx DisplayOrder; // Index within Table's IndexToDisplayOrder[] (column may be reordered by users) + ImGuiTableColumnIdx IndexWithinEnabledSet; // Index within enabled/visible set (<= IndexToDisplayOrder) + ImGuiTableColumnIdx PrevEnabledColumn; // Index of prev enabled/visible column within Columns[], -1 if first enabled/visible column + ImGuiTableColumnIdx NextEnabledColumn; // Index of next enabled/visible column within Columns[], -1 if last enabled/visible column + ImGuiTableColumnIdx SortOrder; // Index of this column within sort specs, -1 if not sorting on this column, 0 for single-sort, may be >0 on multi-sort + ImGuiTableDrawChannelIdx DrawChannelCurrent; // Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx DrawChannelFrozen; // Draw channels for frozen rows (often headers) + ImGuiTableDrawChannelIdx DrawChannelUnfrozen; // Draw channels for unfrozen rows + bool IsEnabled; // IsUserEnabled && (Flags & ImGuiTableColumnFlags_Disabled) == 0 + bool IsUserEnabled; // Is the column not marked Hidden by the user? (unrelated to being off view, e.g. clipped by scrolling). + bool IsUserEnabledNextFrame; + bool IsVisibleX; // Is actually in view (e.g. overlapping the host window clipping rectangle, not scrolled). + bool IsVisibleY; + bool IsRequestOutput; // Return value for TableSetColumnIndex() / TableNextColumn(): whether we request user to output contents or not. + bool IsSkipItems; // Do we want item submissions to this column to be completely ignored (no layout will happen). + bool IsPreserveWidthAuto; + ImS8 NavLayerCurrent; // ImGuiNavLayer in 1 byte + ImU8 AutoFitQueue; // Queue of 8 values for the next 8 frames to request auto-fit + ImU8 CannotSkipItemsQueue; // Queue of 8 values for the next 8 frames to disable Clipped/SkipItem + ImU8 SortDirection : 2; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + ImU8 SortDirectionsAvailCount : 2; // Number of available sort directions (0 to 3) + ImU8 SortDirectionsAvailMask : 4; // Mask of available sort directions (1-bit each) + ImU8 SortDirectionsAvailList; // Ordered list of available sort directions (2-bits each, total 8-bits) + + ImGuiTableColumn() + { + memset(this, 0, sizeof(*this)); + StretchWeight = WidthRequest = -1.0f; + NameOffset = -1; + DisplayOrder = IndexWithinEnabledSet = -1; + PrevEnabledColumn = NextEnabledColumn = -1; + SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + DrawChannelCurrent = DrawChannelFrozen = DrawChannelUnfrozen = (ImU8)-1; + } +}; + +// Transient cell data stored per row. +// sizeof() ~ 6 bytes +struct ImGuiTableCellData { - ImGuiContext &g = *GImGui; - g.CurrentWindow->WriteAccessed = true; - return g.CurrentWindow; -} -IMGUI_API ImGuiWindow *FindWindowByName(const char *name); -IMGUI_API void FocusWindow(ImGuiWindow *window); -IMGUI_API void BringWindowToFront(ImGuiWindow *window); -IMGUI_API void BringWindowToBack(ImGuiWindow *window); -IMGUI_API bool IsWindowChildOf(ImGuiWindow *window, ImGuiWindow *potential_parent); -IMGUI_API bool IsWindowNavFocusable(ImGuiWindow *window); - -IMGUI_API void Initialize(ImGuiContext *context); -IMGUI_API void Shutdown(ImGuiContext *context); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). - -IMGUI_API void MarkIniSettingsDirty(); -IMGUI_API ImGuiSettingsHandler *FindSettingsHandler(const char *type_name); -IMGUI_API ImGuiWindowSettings *FindWindowSettings(ImGuiID id); - -IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow *window); -IMGUI_API ImGuiID GetActiveID(); -IMGUI_API void SetFocusID(ImGuiID id, ImGuiWindow *window); -IMGUI_API void ClearActiveID(); -IMGUI_API void SetHoveredID(ImGuiID id); -IMGUI_API ImGuiID GetHoveredID(); -IMGUI_API void KeepAliveID(ImGuiID id); - -IMGUI_API void ItemSize(const ImVec2 &size, float text_offset_y = 0.0f); -IMGUI_API void ItemSize(const ImRect &bb, float text_offset_y = 0.0f); -IMGUI_API bool ItemAdd(const ImRect &bb, ImGuiID id, const ImRect *nav_bb = NULL); -IMGUI_API bool ItemHoverable(const ImRect &bb, ImGuiID id); -IMGUI_API bool IsClippedEx(const ImRect &bb, ImGuiID id, bool clip_even_when_logged); -IMGUI_API bool FocusableItemRegister(ImGuiWindow *window, ImGuiID id, bool tab_stop = true); // Return true if focus is requested -IMGUI_API void FocusableItemUnregister(ImGuiWindow *window); -IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_x, float default_y); -IMGUI_API float CalcWrapWidthForPos(const ImVec2 &pos, float wrap_pos_x); -IMGUI_API void PushMultiItemsWidths(int components, float width_full = 0.0f); -IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); -IMGUI_API void PopItemFlag(); - -IMGUI_API void SetCurrentFont(ImFont *font); - -IMGUI_API void OpenPopupEx(ImGuiID id); -IMGUI_API void ClosePopup(ImGuiID id); -IMGUI_API void ClosePopupsOverWindow(ImGuiWindow *ref_window); -IMGUI_API bool IsPopupOpen(ImGuiID id); -IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); -IMGUI_API void BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip = true); - -IMGUI_API void NavInitWindow(ImGuiWindow *window, bool force_reinit); -IMGUI_API void NavMoveRequestCancel(); -IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again. - -IMGUI_API float GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode); -IMGUI_API ImVec2 GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor = 0.0f, float fast_factor = 0.0f); -IMGUI_API int CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate); - -IMGUI_API void Scrollbar(ImGuiLayoutType direction); -IMGUI_API void VerticalSeparator(); // Vertical separator, for menu bars (use current line height). not exposed because it is misleading what it doesn't have an effect on regular layout. -IMGUI_API bool SplitterBehavior(ImGuiID id, const ImRect &bb, ImGuiAxis axis, float *size1, float *size2, float min_size1, float min_size2, float hover_extend = 0.0f); - -IMGUI_API bool BeginDragDropTargetCustom(const ImRect &bb, ImGuiID id); -IMGUI_API void ClearDragDrop(); -IMGUI_API bool IsDragDropPayloadBeingAccepted(); - -// FIXME-WIP: New Columns API -IMGUI_API void BeginColumns(const char *str_id, int count, ImGuiColumnsFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). -IMGUI_API void EndColumns(); // close columns -IMGUI_API void PushColumnClipRect(int column_index = -1); - -// NB: All position are in absolute pixels coordinates (never using window coordinates internally) -// AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. -IMGUI_API void RenderText(ImVec2 pos, const char *text, const char *text_end = NULL, bool hide_text_after_hash = true); -IMGUI_API void RenderTextWrapped(ImVec2 pos, const char *text, const char *text_end, float wrap_width); -IMGUI_API void RenderTextClipped(const ImVec2 &pos_min, const ImVec2 &pos_max, const char *text, const char *text_end, const ImVec2 *text_size_if_known, const ImVec2 &align = ImVec2(0, 0), const ImRect *clip_rect = NULL); -IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); -IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); -IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, int rounding_corners_flags = ~0); -IMGUI_API void RenderTriangle(ImVec2 pos, ImGuiDir dir, float scale = 1.0f); -IMGUI_API void RenderBullet(ImVec2 pos); -IMGUI_API void RenderCheckMark(ImVec2 pos, ImU32 col, float sz); -IMGUI_API void RenderNavHighlight(const ImRect &bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_TypeDefault); // Navigation highlight -IMGUI_API void RenderRectFilledRangeH(ImDrawList *draw_list, const ImRect &rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); -IMGUI_API const char *FindRenderedTextEnd(const char *text, const char *text_end = NULL); // Find the optional ## from which we stop displaying text. - -IMGUI_API bool ButtonBehavior(const ImRect &bb, ImGuiID id, bool *out_hovered, bool *out_held, ImGuiButtonFlags flags = 0); -IMGUI_API bool ButtonEx(const char *label, const ImVec2 &size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); -IMGUI_API bool CloseButton(ImGuiID id, const ImVec2 &pos, float radius); -IMGUI_API bool ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags = 0); - -IMGUI_API bool SliderBehavior(const ImRect &frame_bb, ImGuiID id, float *v, float v_min, float v_max, float power, int decimal_precision, ImGuiSliderFlags flags = 0); -IMGUI_API bool SliderFloatN(const char *label, float *v, int components, float v_min, float v_max, const char *display_format, float power); -IMGUI_API bool SliderIntN(const char *label, int *v, int components, int v_min, int v_max, const char *display_format); - -IMGUI_API bool DragBehavior(const ImRect &frame_bb, ImGuiID id, float *v, float v_speed, float v_min, float v_max, int decimal_precision, float power); -IMGUI_API bool DragFloatN(const char *label, float *v, int components, float v_speed, float v_min, float v_max, const char *display_format, float power); -IMGUI_API bool DragIntN(const char *label, int *v, int components, float v_speed, int v_min, int v_max, const char *display_format); - -IMGUI_API bool InputTextEx(const char *label, char *buf, int buf_size, const ImVec2 &size_arg, ImGuiInputTextFlags flags, ImGuiTextEditCallback callback = NULL, void *user_data = NULL); -IMGUI_API bool InputFloatN(const char *label, float *v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags); -IMGUI_API bool InputIntN(const char *label, int *v, int components, ImGuiInputTextFlags extra_flags); -IMGUI_API bool InputScalarEx(const char *label, ImGuiDataType data_type, void *data_ptr, void *step_ptr, void *step_fast_ptr, const char *scalar_format, ImGuiInputTextFlags extra_flags); -IMGUI_API bool InputScalarAsWidgetReplacement(const ImRect &aabb, const char *label, ImGuiDataType data_type, void *data_ptr, ImGuiID id, int decimal_precision); - -IMGUI_API void ColorTooltip(const char *text, const float *col, ImGuiColorEditFlags flags); -IMGUI_API void ColorEditOptionsPopup(const float *col, ImGuiColorEditFlags flags); - -IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char *label, const char *label_end = NULL); -IMGUI_API bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0); // Consume previous SetNextTreeNodeOpened() data, if any. May return true when logging -IMGUI_API void TreePushRawID(ImGuiID id); - -IMGUI_API void PlotEx(ImGuiPlotType plot_type, const char *label, float (*values_getter)(void *data, int idx), void *data, int values_count, int values_offset, const char *overlay_text, float scale_min, float scale_max, ImVec2 graph_size); - -IMGUI_API int ParseFormatPrecision(const char *fmt, int default_value); -IMGUI_API float RoundScalar(float value, int decimal_precision); - -// Shade functions -IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawVert *vert_start, ImDrawVert *vert_end, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); -IMGUI_API void ShadeVertsLinearAlphaGradientForLeftToRightText(ImDrawVert *vert_start, ImDrawVert *vert_end, float gradient_p0_x, float gradient_p1_x); -IMGUI_API void ShadeVertsLinearUV(ImDrawVert *vert_start, ImDrawVert *vert_end, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, bool clamp); - -} // namespace ImGui - -// ImFontAtlas internals -IMGUI_API bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas *atlas); -IMGUI_API void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas *atlas); -IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas *atlas, ImFont *font, ImFontConfig *font_config, float ascent, float descent); -IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas *atlas, void *spc); -IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas *atlas); -IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); -IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char *pixels, int x, int y, int w, int h, int stride); - -#ifdef __clang__ -# pragma clang diagnostic pop + ImU32 BgColor; // Actual color + ImGuiTableColumnIdx Column; // Column number +}; + +// Parameters for TableAngledHeadersRowEx() +// This may end up being refactored for more general purpose. +// sizeof() ~ 12 bytes +struct ImGuiTableHeaderData +{ + ImGuiTableColumnIdx Index; // Column index + ImU32 TextColor; + ImU32 BgColor0; + ImU32 BgColor1; +}; + +// Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?) +// sizeof() ~ 24 bytes +struct ImGuiTableInstanceData +{ + ImGuiID TableInstanceID; + float LastOuterHeight; // Outer height from last frame + float LastTopHeadersRowHeight; // Height of first consecutive header rows from last frame (FIXME: this is used assuming consecutive headers are in same frozen set) + float LastFrozenHeight; // Height of frozen section from last frame + int HoveredRowLast; // Index of row which was hovered last frame. + int HoveredRowNext; // Index of row hovered this frame, set after encountering it. + + ImGuiTableInstanceData() { TableInstanceID = 0; LastOuterHeight = LastTopHeadersRowHeight = LastFrozenHeight = 0.0f; HoveredRowLast = HoveredRowNext = -1; } +}; + +// sizeof() ~ 592 bytes + heap allocs described in TableBeginInitMemory() +struct IMGUI_API ImGuiTable +{ + ImGuiID ID; + ImGuiTableFlags Flags; + void* RawData; // Single allocation to hold Columns[], DisplayOrderToIndex[] and RowCellData[] + ImGuiTableTempData* TempData; // Transient data while table is active. Point within g.CurrentTableStack[] + ImSpan Columns; // Point within RawData[] + ImSpan DisplayOrderToIndex; // Point within RawData[]. Store display order of columns (when not reordered, the values are 0...Count-1) + ImSpan RowCellData; // Point within RawData[]. Store cells background requests for current row. + ImBitArrayPtr EnabledMaskByDisplayOrder; // Column DisplayOrder -> IsEnabled map + ImBitArrayPtr EnabledMaskByIndex; // Column Index -> IsEnabled map (== not hidden by user/api) in a format adequate for iterating column without touching cold data + ImBitArrayPtr VisibleMaskByIndex; // Column Index -> IsVisibleX|IsVisibleY map (== not hidden by user/api && not hidden by scrolling/cliprect) + ImGuiTableFlags SettingsLoadedFlags; // Which data were loaded from the .ini file (e.g. when order is not altered we won't save order) + int SettingsOffset; // Offset in g.SettingsTables + int LastFrameActive; + int ColumnsCount; // Number of columns declared in BeginTable() + int CurrentRow; + int CurrentColumn; + ImS16 InstanceCurrent; // Count of BeginTable() calls with same ID in the same frame (generally 0). This is a little bit similar to BeginCount for a window, but multiple table with same ID look are multiple tables, they are just synched. + ImS16 InstanceInteracted; // Mark which instance (generally 0) of the same ID is being interacted with + float RowPosY1; + float RowPosY2; + float RowMinHeight; // Height submitted to TableNextRow() + float RowCellPaddingY; // Top and bottom padding. Reloaded during row change. + float RowTextBaseline; + float RowIndentOffsetX; + ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ + ImGuiTableRowFlags LastRowFlags : 16; + int RowBgColorCounter; // Counter for alternating background colors (can be fast-forwarded by e.g clipper), not same as CurrentRow because header rows typically don't increase this. + ImU32 RowBgColor[2]; // Background color override for current row. + ImU32 BorderColorStrong; + ImU32 BorderColorLight; + float BorderX1; + float BorderX2; + float HostIndentX; + float MinColumnWidth; + float OuterPaddingX; + float CellPaddingX; // Padding from each borders. Locked in BeginTable()/Layout. + float CellSpacingX1; // Spacing between non-bordered cells. Locked in BeginTable()/Layout. + float CellSpacingX2; + float InnerWidth; // User value passed to BeginTable(), see comments at the top of BeginTable() for details. + float ColumnsGivenWidth; // Sum of current column width + float ColumnsAutoFitWidth; // Sum of ideal column width in order nothing to be clipped, used for auto-fitting and content width submission in outer window + float ColumnsStretchSumWeights; // Sum of weight of all enabled stretching columns + float ResizedColumnNextWidth; + float ResizeLockMinContentsX2; // Lock minimum contents width while resizing down in order to not create feedback loops. But we allow growing the table. + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + float AngledHeadersHeight; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + float AngledHeadersSlope; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + ImRect OuterRect; // Note: for non-scrolling table, OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). + ImRect InnerRect; // InnerRect but without decoration. As with OuterRect, for non-scrolling tables, InnerRect.Max.y is + ImRect WorkRect; + ImRect InnerClipRect; + ImRect BgClipRect; // We use this to cpu-clip cell background color fill, evolve during the frame as we cross frozen rows boundaries + ImRect Bg0ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG0/1 channel. This tends to be == OuterWindow->ClipRect at BeginTable() because output in BG0/BG1 is cpu-clipped + ImRect Bg2ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG2 channel. This tends to be a correct, tight-fit, because output to BG2 are done by widgets relying on regular ClipRect. + ImRect HostClipRect; // This is used to check if we can eventually merge our columns draw calls into the current draw call of the current window. + ImRect HostBackupInnerClipRect; // Backup of InnerWindow->ClipRect during PushTableBackground()/PopTableBackground() + ImGuiWindow* OuterWindow; // Parent window for the table + ImGuiWindow* InnerWindow; // Window holding the table data (== OuterWindow or a child window) + ImGuiTextBuffer ColumnsNames; // Contiguous buffer holding columns names + ImDrawListSplitter* DrawSplitter; // Shortcut to TempData->DrawSplitter while in table. Isolate draw commands per columns to avoid switching clip rect constantly + ImGuiTableInstanceData InstanceDataFirst; + ImVector InstanceDataExtra; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableColumnSortSpecs SortSpecsSingle; + ImVector SortSpecsMulti; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() + ImGuiTableColumnIdx SortSpecsCount; + ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns using fixed width (<= ColumnsCount) + ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() + ImGuiTableColumnIdx AngledHeadersCount; // Count columns with angled headers + ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! + ImGuiTableColumnIdx HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImGuiTableColumnIdx HighlightColumnHeader; // Index of column which should be highlighted. + ImGuiTableColumnIdx AutoFitSingleColumn; // Index of single column requesting auto-fit. + ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. + ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. + ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. + ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumnDir; // -1 or +1 + ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. + ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. + ImGuiTableColumnIdx LeftMostStretchedColumn; // Index of left-most stretched column. + ImGuiTableColumnIdx RightMostStretchedColumn; // Index of right-most stretched column. + ImGuiTableColumnIdx ContextPopupColumn; // Column right-clicked on, of -1 if opening context menu from a neutral/empty spot + ImGuiTableColumnIdx FreezeRowsRequest; // Requested frozen rows count + ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count + ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row + ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. + ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; + bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. + bool IsInsideRow; // Set when inside TableBeginRow()/TableEndRow(). + bool IsInitializing; + bool IsSortSpecsDirty; + bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. + bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). + bool DisableDefaultContextMenu; // Disable default context menu contents. You may submit your own using TableBeginContextMenuPopup()/EndPopup() + bool IsSettingsRequestLoad; + bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. + bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) + bool IsResetAllRequest; + bool IsResetDisplayOrderRequest; + bool IsUnfrozenRows; // Set when we got past the frozen row. + bool IsDefaultSizingPolicy; // Set if user didn't explicitly set a sizing policy in BeginTable() + bool IsActiveIdAliveBeforeTable; + bool IsActiveIdInTable; + bool HasScrollbarYCurr; // Whether ANY instance of this table had a vertical scrollbar during the current frame. + bool HasScrollbarYPrev; // Whether ANY instance of this table had a vertical scrollbar during the previous. + bool MemoryCompacted; + bool HostSkipItems; // Backup of InnerWindow->SkipItem at the end of BeginTable(), because we will overwrite InnerWindow->SkipItem on a per-column basis + + ImGuiTable() { memset(this, 0, sizeof(*this)); LastFrameActive = -1; } + ~ImGuiTable() { IM_FREE(RawData); } +}; + +// Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). +// - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. +// - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. +// FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs. +// sizeof() ~ 136 bytes. +struct IMGUI_API ImGuiTableTempData +{ + int TableIndex; // Index in g.Tables.Buf[] pool + float LastTimeActive; // Last timestamp this structure was used + float AngledHeadersExtraWidth; // Used in EndTable() + ImVector AngledHeadersRequests; // Used in TableAngledHeadersRow() + + ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() + ImDrawListSplitter DrawSplitter; + + ImRect HostBackupWorkRect; // Backup of InnerWindow->WorkRect at the end of BeginTable() + ImRect HostBackupParentWorkRect; // Backup of InnerWindow->ParentWorkRect at the end of BeginTable() + ImVec2 HostBackupPrevLineSize; // Backup of InnerWindow->DC.PrevLineSize at the end of BeginTable() + ImVec2 HostBackupCurrLineSize; // Backup of InnerWindow->DC.CurrLineSize at the end of BeginTable() + ImVec2 HostBackupCursorMaxPos; // Backup of InnerWindow->DC.CursorMaxPos at the end of BeginTable() + ImVec1 HostBackupColumnsOffset; // Backup of OuterWindow->DC.ColumnsOffset at the end of BeginTable() + float HostBackupItemWidth; // Backup of OuterWindow->DC.ItemWidth at the end of BeginTable() + int HostBackupItemWidthStackSize;//Backup of OuterWindow->DC.ItemWidthStack.Size at the end of BeginTable() + + ImGuiTableTempData() { memset(this, 0, sizeof(*this)); LastTimeActive = -1.0f; } +}; + +// sizeof() ~ 12 +struct ImGuiTableColumnSettings +{ + float WidthOrWeight; + ImGuiID UserID; + ImGuiTableColumnIdx Index; + ImGuiTableColumnIdx DisplayOrder; + ImGuiTableColumnIdx SortOrder; + ImU8 SortDirection : 2; + ImU8 IsEnabled : 1; // "Visible" in ini file + ImU8 IsStretch : 1; + + ImGuiTableColumnSettings() + { + WidthOrWeight = 0.0f; + UserID = 0; + Index = -1; + DisplayOrder = SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + IsEnabled = 1; + IsStretch = 0; + } +}; + +// This is designed to be stored in a single ImChunkStream (1 header followed by N ImGuiTableColumnSettings, etc.) +struct ImGuiTableSettings +{ + ImGuiID ID; // Set to 0 to invalidate/delete the setting + ImGuiTableFlags SaveFlags; // Indicate data we want to save using the Resizable/Reorderable/Sortable/Hideable flags (could be using its own flags..) + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + ImGuiTableColumnIdx ColumnsCount; + ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + + ImGuiTableSettings() { memset(this, 0, sizeof(*this)); } + ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGui internal API +// No guarantee of forward compatibility here! +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Windows + // We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) + // If this ever crashes because g.CurrentWindow is NULL, it means that either: + // - ImGui::NewFrame() has never been called, which is illegal. + // - You are calling ImGui functions after ImGui::EndFrame()/ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. + inline ImGuiWindow* GetCurrentWindowRead() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } + inline ImGuiWindow* GetCurrentWindow() { ImGuiContext& g = *GImGui; g.CurrentWindow->WriteAccessed = true; return g.CurrentWindow; } + IMGUI_API ImGuiWindow* FindWindowByID(ImGuiID id); + IMGUI_API ImGuiWindow* FindWindowByName(const char* name); + IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); + IMGUI_API void UpdateWindowSkipRefresh(ImGuiWindow* window); + IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy); + IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API bool IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below); + IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); + IMGUI_API void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond = 0); + IMGUI_API void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond = 0); + IMGUI_API void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond = 0); + IMGUI_API void SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size); + IMGUI_API void SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window); + inline void SetWindowParentWindowForFocusRoute(ImGuiWindow* window, ImGuiWindow* parent_window) { window->ParentWindowForFocusRoute = parent_window; } + inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } + inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } + inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } + + // Windows: Display Order and Focus Order + IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); + IMGUI_API void FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags); + IMGUI_API void BringWindowToFocusFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBack(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* above_window); + IMGUI_API int FindWindowDisplayIndex(ImGuiWindow* window); + IMGUI_API ImGuiWindow* FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* window); + + // Windows: Idle, Refresh Policies [EXPERIMENTAL] + IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); + + // Fonts, drawing + IMGUI_API void SetCurrentFont(ImFont* font); + inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } + inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { IM_UNUSED(window); return GetForegroundDrawList(); } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches. + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API void AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list); + + // Init + IMGUI_API void Initialize(); + IMGUI_API void Shutdown(); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). + + // NewFrame + IMGUI_API void UpdateInputEvents(bool trickle_fast_inputs); + IMGUI_API void UpdateHoveredWindowAndCaptureFlags(); + IMGUI_API void FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window); + IMGUI_API void StartMouseMovingWindow(ImGuiWindow* window); + IMGUI_API void UpdateMouseMovingWindowNewFrame(); + IMGUI_API void UpdateMouseMovingWindowEndFrame(); + + // Generic context hooks + IMGUI_API ImGuiID AddContextHook(ImGuiContext* context, const ImGuiContextHook* hook); + IMGUI_API void RemoveContextHook(ImGuiContext* context, ImGuiID hook_to_remove); + IMGUI_API void CallContextHooks(ImGuiContext* context, ImGuiContextHookType type); + + // Viewports + IMGUI_API void SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + + // Settings + IMGUI_API void MarkIniSettingsDirty(); + IMGUI_API void MarkIniSettingsDirty(ImGuiWindow* window); + IMGUI_API void ClearIniSettings(); + IMGUI_API void AddSettingsHandler(const ImGuiSettingsHandler* handler); + IMGUI_API void RemoveSettingsHandler(const char* type_name); + IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); + + // Settings - Windows + IMGUI_API ImGuiWindowSettings* CreateNewWindowSettings(const char* name); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByID(ImGuiID id); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByWindow(ImGuiWindow* window); + IMGUI_API void ClearWindowSettings(const char* name); + + // Localization + IMGUI_API void LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count); + inline const char* LocalizeGetMsg(ImGuiLocKey key) { ImGuiContext& g = *GImGui; const char* msg = g.LocalizationTable[key]; return msg ? msg : "*Missing Text*"; } + + // Scrolling + IMGUI_API void SetScrollX(ImGuiWindow* window, float scroll_x); + IMGUI_API void SetScrollY(ImGuiWindow* window, float scroll_y); + IMGUI_API void SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio); + IMGUI_API void SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio); + + // Early work-in-progress API (ScrollToItem() will become public) + IMGUI_API void ScrollToItem(ImGuiScrollFlags flags = 0); + IMGUI_API void ScrollToRect(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); + IMGUI_API ImVec2 ScrollToRectEx(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); +//#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& rect) { ScrollToRect(window, rect, ImGuiScrollFlags_KeepVisibleEdgeY); } +//#endif + + // Basic Accessors + inline ImGuiItemStatusFlags GetItemStatusFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.StatusFlags; } + inline ImGuiItemFlags GetItemFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.InFlags; } + inline ImGuiID GetActiveID() { ImGuiContext& g = *GImGui; return g.ActiveId; } + inline ImGuiID GetFocusID() { ImGuiContext& g = *GImGui; return g.NavId; } + IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void SetFocusID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void ClearActiveID(); + IMGUI_API ImGuiID GetHoveredID(); + IMGUI_API void SetHoveredID(ImGuiID id); + IMGUI_API void KeepAliveID(ImGuiID id); + IMGUI_API void MarkItemEdited(ImGuiID id); // Mark data associated to given item as "edited", used by IsItemDeactivatedAfterEdit() function. + IMGUI_API void PushOverrideID(ImGuiID id); // Push given value as-is at the top of the ID stack (whereas PushID combines old and new hashes) + IMGUI_API ImGuiID GetIDWithSeed(const char* str_id_begin, const char* str_id_end, ImGuiID seed); + IMGUI_API ImGuiID GetIDWithSeed(int n, ImGuiID seed); + + // Basic Helpers for widget code + IMGUI_API void ItemSize(const ImVec2& size, float text_baseline_y = -1.0f); + inline void ItemSize(const ImRect& bb, float text_baseline_y = -1.0f) { ItemSize(bb.GetSize(), text_baseline_y); } // FIXME: This is a misleading API since we expect CursorPos to be bb.Min. + IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags); + IMGUI_API bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags = 0); + IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id); + IMGUI_API void SetLastItemData(ImGuiID item_id, ImGuiItemFlags in_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect); + IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h); + IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); + IMGUI_API void PushMultiItemsWidths(int components, float width_full); + IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess); + + // Parameter stacks (shared) + IMGUI_API const ImGuiDataVarInfo* GetStyleVarInfo(ImGuiStyleVar idx); + IMGUI_API void BeginDisabledOverrideReenable(); + IMGUI_API void EndDisabledOverrideReenable(); + + // Logging/Capture + IMGUI_API void LogBegin(ImGuiLogType type, int auto_open_depth); // -> BeginCapture() when we design v2 api, for now stay under the radar by using the old name. + IMGUI_API void LogToBuffer(int auto_open_depth = -1); // Start logging/capturing to internal buffer + IMGUI_API void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL); + IMGUI_API void LogSetNextTextDecoration(const char* prefix, const char* suffix); + + // Childs + IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); + + // Popups, Modals + IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags); + IMGUI_API void OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_None); + IMGUI_API void ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsExceptModals(); + IMGUI_API bool IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags); + IMGUI_API ImRect GetPopupAllowedExtentRect(ImGuiWindow* window); + IMGUI_API ImGuiWindow* GetTopMostPopupModal(); + IMGUI_API ImGuiWindow* GetTopMostAndVisiblePopupModal(); + IMGUI_API ImGuiWindow* FindBlockingModal(ImGuiWindow* window); + IMGUI_API ImVec2 FindBestWindowPosForPopup(ImGuiWindow* window); + IMGUI_API ImVec2 FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy); + + // Tooltips + IMGUI_API bool BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginTooltipHidden(); + + // Menus + IMGUI_API bool BeginViewportSideBar(const char* name, ImGuiViewport* viewport, ImGuiDir dir, float size, ImGuiWindowFlags window_flags); + IMGUI_API bool BeginMenuEx(const char* label, const char* icon, bool enabled = true); + IMGUI_API bool MenuItemEx(const char* label, const char* icon, const char* shortcut = NULL, bool selected = false, bool enabled = true); + + // Combos + IMGUI_API bool BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags); + IMGUI_API bool BeginComboPreview(); + IMGUI_API void EndComboPreview(); + + // Gamepad/Keyboard Navigation + IMGUI_API void NavInitWindow(ImGuiWindow* window, bool force_reinit); + IMGUI_API void NavInitRequestApplyResult(); + IMGUI_API bool NavMoveRequestButNoResultYet(); + IMGUI_API void NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); + IMGUI_API void NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data); + IMGUI_API void NavMoveRequestCancel(); + IMGUI_API void NavMoveRequestApplyResult(); + IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags); + IMGUI_API void NavHighlightActivated(ImGuiID id); + IMGUI_API void NavClearPreferredPosForAxis(ImGuiAxis axis); + IMGUI_API void NavRestoreHighlightAfterMove(); + IMGUI_API void NavUpdateCurrentWindowIsScrollPushableX(); + IMGUI_API void SetNavWindow(ImGuiWindow* window); + IMGUI_API void SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel); + IMGUI_API void SetNavFocusScope(ImGuiID focus_scope_id); + + // Focus/Activation + // This should be part of a larger set of API: FocusItem(offset = -1), FocusItemByID(id), ActivateItem(offset = -1), ActivateItemByID(id) etc. which are + // much harder to design and implement than expected. I have a couple of private branches on this matter but it's not simple. For now implementing the easy ones. + IMGUI_API void FocusItem(); // Focus last item (no selection/activation). + IMGUI_API void ActivateItemByID(ImGuiID id); // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again. + + // Inputs + // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions. + inline bool IsNamedKey(ImGuiKey key) { return key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END; } + inline bool IsNamedKeyOrMod(ImGuiKey key) { return (key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END) || key == ImGuiMod_Ctrl || key == ImGuiMod_Shift || key == ImGuiMod_Alt || key == ImGuiMod_Super; } + inline bool IsLegacyKey(ImGuiKey key) { return key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_LegacyNativeKey_END; } + inline bool IsKeyboardKey(ImGuiKey key) { return key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END; } + inline bool IsGamepadKey(ImGuiKey key) { return key >= ImGuiKey_Gamepad_BEGIN && key < ImGuiKey_Gamepad_END; } + inline bool IsMouseKey(ImGuiKey key) { return key >= ImGuiKey_Mouse_BEGIN && key < ImGuiKey_Mouse_END; } + inline bool IsAliasKey(ImGuiKey key) { return key >= ImGuiKey_Aliases_BEGIN && key < ImGuiKey_Aliases_END; } + inline bool IsLRModKey(ImGuiKey key) { return key >= ImGuiKey_LeftCtrl && key <= ImGuiKey_RightSuper; } + ImGuiKeyChord FixupKeyChord(ImGuiKeyChord key_chord); + inline ImGuiKey ConvertSingleModFlagToKey(ImGuiKey key) + { + if (key == ImGuiMod_Ctrl) return ImGuiKey_ReservedForModCtrl; + if (key == ImGuiMod_Shift) return ImGuiKey_ReservedForModShift; + if (key == ImGuiMod_Alt) return ImGuiKey_ReservedForModAlt; + if (key == ImGuiMod_Super) return ImGuiKey_ReservedForModSuper; + return key; + } + + IMGUI_API ImGuiKeyData* GetKeyData(ImGuiContext* ctx, ImGuiKey key); + inline ImGuiKeyData* GetKeyData(ImGuiKey key) { ImGuiContext& g = *GImGui; return GetKeyData(&g, key); } + IMGUI_API const char* GetKeyChordName(ImGuiKeyChord key_chord); + inline ImGuiKey MouseButtonToKey(ImGuiMouseButton button) { IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); return (ImGuiKey)(ImGuiKey_MouseLeft + button); } + IMGUI_API bool IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold = -1.0f); + IMGUI_API ImVec2 GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down); + IMGUI_API float GetNavTweakPressedAmount(ImGuiAxis axis); + IMGUI_API int CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate); + IMGUI_API void GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate); + IMGUI_API void TeleportMousePos(const ImVec2& pos); + IMGUI_API void SetActiveIdUsingAllKeyboardKeys(); + inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } + + // [EXPERIMENTAL] Low-Level: Key/Input Ownership + // - The idea is that instead of "eating" a given input, we can link to an owner id. + // - Ownership is most often claimed as a result of reacting to a press/down event (but occasionally may be claimed ahead). + // - Input queries can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Legacy input queries (without specifying an owner or _Any or _None) are equivalent to using ImGuiKeyOwner_Any (== 0). + // - Input ownership is automatically released on the frame after a key is released. Therefore: + // - for ownership registration happening as a result of a down/press event, the SetKeyOwner() call may be done once (common case). + // - for ownership registration happening ahead of a down/press event, the SetKeyOwner() call needs to be made every frame (happens if e.g. claiming ownership on hover). + // - SetItemKeyOwner() is a shortcut for common simple case. A custom widget will probably want to call SetKeyOwner() multiple times directly based on its interaction state. + // - This is marked experimental because not all widgets are fully honoring the Set/Test idioms. We will need to move forward step by step. + // Please open a GitHub Issue to submit your usage scenario or if there's a use case you need solved. + IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key); + IMGUI_API void SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetKeyOwnersForKeyChord(ImGuiKeyChord key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); // Set key owner to last item if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + IMGUI_API bool TestKeyOwner(ImGuiKey key, ImGuiID owner_id); // Test that key is either not owned, either owned by 'owner_id' + inline ImGuiKeyOwnerData* GetKeyOwnerData(ImGuiContext* ctx, ImGuiKey key) { if (key & ImGuiMod_Mask_) key = ConvertSingleModFlagToKey(key); IM_ASSERT(IsNamedKey(key)); return &ctx->KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; } + + // [EXPERIMENTAL] High-Level: Input Access functions w/ support for Key/Input Ownership + // - Important: legacy IsKeyPressed(ImGuiKey, bool repeat=true) _DEFAULTS_ to repeat, new IsKeyPressed() requires _EXPLICIT_ ImGuiInputFlags_Repeat flag. + // - Expected to be later promoted to public API, the prototypes are designed to replace existing ones (since owner_id can default to Any == 0) + // - Specifying a value for 'ImGuiID owner' will test that EITHER the key is NOT owned (UNLESS locked), EITHER the key is owned by 'owner'. + // Legacy functions use ImGuiKeyOwner_Any meaning that they typically ignore ownership, unless a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. + // - Binding generators may want to ignore those for now, or suffix them with Ex() until we decide if this gets moved into public API. + IMGUI_API bool IsKeyDown(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); // Important: when transitioning from old to new IsKeyPressed(): old API has "bool repeat = true", so would default to repeat. New API requiress explicit ImGuiInputFlags_Repeat. + IMGUI_API bool IsKeyReleased(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id); + + // Shortcut Testing & Routing + // - Set Shortcut() and SetNextItemShortcut() in imgui.h + // - When a policy (except for ImGuiInputFlags_RouteAlways *) is set, Shortcut() will register itself with SetShortcutRouting(), + // allowing the system to decide where to route the input among other route-aware calls. + // (* using ImGuiInputFlags_RouteAlways is roughly equivalent to calling IsKeyChordPressed(key) and bypassing route registration and check) + // - When using one of the routing option: + // - The default route is ImGuiInputFlags_RouteFocused (accept inputs if window is in focus stack. Deep-most focused window takes inputs. ActiveId takes inputs over deep-most focused window.) + // - Routes are requested given a chord (key + modifiers) and a routing policy. + // - Routes are resolved during NewFrame(): if keyboard modifiers are matching current ones: SetKeyOwner() is called + route is granted for the frame. + // - Each route may be granted to a single owner. When multiple requests are made we have policies to select the winning route (e.g. deep most window). + // - Multiple read sites may use the same owner id can all access the granted route. + // - When owner_id is 0 we use the current Focus Scope ID as a owner ID in order to identify our location. + // - You can chain two unrelated windows in the focus stack using SetWindowParentWindowForFocusRoute() + // e.g. if you have a tool window associated to a document, and you want document shortcuts to run when the tool is focused. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); + IMGUI_API bool SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); // owner_id needs to be explicit and cannot be 0 + IMGUI_API bool TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id); + IMGUI_API ImGuiKeyRoutingData* GetShortcutRoutingData(ImGuiKeyChord key_chord); + + // [EXPERIMENTAL] Focus Scope + // This is generally used to identify a unique input location (for e.g. a selection set) + // There is one per window (automatically set in Begin), but: + // - Selection patterns generally need to react (e.g. clear a selection) when landing on one item of the set. + // So in order to identify a set multiple lists in same window may each need a focus scope. + // If you imagine an hypothetical BeginSelectionGroup()/EndSelectionGroup() api, it would likely call PushFocusScope()/EndFocusScope() + // - Shortcut routing also use focus scope as a default location identifier if an owner is not provided. + // We don't use the ID Stack for this as it is common to want them separate. + IMGUI_API void PushFocusScope(ImGuiID id); + IMGUI_API void PopFocusScope(); + inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope() + + // Drag and Drop + IMGUI_API bool IsDragDropActive(); + IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); + IMGUI_API void ClearDragDrop(); + IMGUI_API bool IsDragDropPayloadBeingAccepted(); + IMGUI_API void RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect); + + // Typing-Select API + IMGUI_API ImGuiTypingSelectRequest* GetTypingSelectRequest(ImGuiTypingSelectFlags flags = ImGuiTypingSelectFlags_None); + IMGUI_API int TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); + + // Box-Select API + IMGUI_API bool BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags); + IMGUI_API void EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags); + + // Multi-Select API + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); + IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + IMGUI_API void MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected); + IMGUI_API void MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item); + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } + inline ImGuiMultiSelectState* GetMultiSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return g.MultiSelectStorage.GetByKey(id); } + + // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) + IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); + IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). + IMGUI_API void EndColumns(); // close columns + IMGUI_API void PushColumnClipRect(int column_index); + IMGUI_API void PushColumnsBackground(); + IMGUI_API void PopColumnsBackground(); + IMGUI_API ImGuiID GetColumnsID(const char* str_id, int count); + IMGUI_API ImGuiOldColumns* FindOrCreateColumns(ImGuiWindow* window, ImGuiID id); + IMGUI_API float GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm); + IMGUI_API float GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset); + + // Tables: Candidates for public API + IMGUI_API void TableOpenContextMenu(int column_n = -1); + IMGUI_API void TableSetColumnWidth(int column_n, float width); + IMGUI_API void TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs); + IMGUI_API int TableGetHoveredRow(); // Retrieve *PREVIOUS FRAME* hovered row. This difference with TableGetHoveredColumn() is the reason why this is not public yet. + IMGUI_API float TableGetHeaderRowHeight(); + IMGUI_API float TableGetHeaderAngledMaxLabelWidth(); + IMGUI_API void TablePushBackgroundChannel(); + IMGUI_API void TablePopBackgroundChannel(); + IMGUI_API void TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count); + + // Tables: Internals + inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } + IMGUI_API ImGuiTable* TableFindByID(ImGuiID id); + IMGUI_API bool BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + IMGUI_API void TableBeginInitMemory(ImGuiTable* table, int columns_count); + IMGUI_API void TableBeginApplyRequests(ImGuiTable* table); + IMGUI_API void TableSetupDrawChannels(ImGuiTable* table); + IMGUI_API void TableUpdateLayout(ImGuiTable* table); + IMGUI_API void TableUpdateBorders(ImGuiTable* table); + IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); + IMGUI_API void TableDrawBorders(ImGuiTable* table); + IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display); + IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table); + IMGUI_API void TableMergeDrawChannels(ImGuiTable* table); + inline ImGuiTableInstanceData* TableGetInstanceData(ImGuiTable* table, int instance_no) { if (instance_no == 0) return &table->InstanceDataFirst; return &table->InstanceDataExtra[instance_no - 1]; } + inline ImGuiID TableGetInstanceID(ImGuiTable* table, int instance_no) { return TableGetInstanceData(table, instance_no)->TableInstanceID; } + IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); + IMGUI_API void TableSortSpecsBuild(ImGuiTable* table); + IMGUI_API ImGuiSortDirection TableGetColumnNextSortDirection(ImGuiTableColumn* column); + IMGUI_API void TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API float TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API void TableBeginRow(ImGuiTable* table); + IMGUI_API void TableEndRow(ImGuiTable* table); + IMGUI_API void TableBeginCell(ImGuiTable* table, int column_n); + IMGUI_API void TableEndCell(ImGuiTable* table); + IMGUI_API ImRect TableGetCellBgRect(const ImGuiTable* table, int column_n); + IMGUI_API const char* TableGetColumnName(const ImGuiTable* table, int column_n); + IMGUI_API ImGuiID TableGetColumnResizeID(ImGuiTable* table, int column_n, int instance_no = 0); + IMGUI_API float TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); + IMGUI_API void TableRemove(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); + IMGUI_API void TableGcCompactSettings(); + + // Tables: Settings + IMGUI_API void TableLoadSettings(ImGuiTable* table); + IMGUI_API void TableSaveSettings(ImGuiTable* table); + IMGUI_API void TableResetSettings(ImGuiTable* table); + IMGUI_API ImGuiTableSettings* TableGetBoundSettings(ImGuiTable* table); + IMGUI_API void TableSettingsAddSettingsHandler(); + IMGUI_API ImGuiTableSettings* TableSettingsCreate(ImGuiID id, int columns_count); + IMGUI_API ImGuiTableSettings* TableSettingsFindByID(ImGuiID id); + + // Tab Bars + inline ImGuiTabBar* GetCurrentTabBar() { ImGuiContext& g = *GImGui; return g.CurrentTabBar; } + IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); + IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API ImGuiTabItem* TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order); + IMGUI_API ImGuiTabItem* TabBarGetCurrentTab(ImGuiTabBar* tab_bar); + inline int TabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { return tab_bar->Tabs.index_from_ptr(tab); } + IMGUI_API const char* TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API void TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset); + IMGUI_API void TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, ImVec2 mouse_pos); + IMGUI_API bool TabBarProcessReorder(ImGuiTabBar* tab_bar); + IMGUI_API bool TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window); + IMGUI_API ImVec2 TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker); + IMGUI_API ImVec2 TabItemCalcSize(ImGuiWindow* window); + IMGUI_API void TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col); + IMGUI_API void TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped); + + // Render helpers + // AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. + // NB: All position are in absolute pixels coordinates (we are never using window coordinates internally) + IMGUI_API void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true); + IMGUI_API void RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known); + IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders = true, float rounding = 0.0f); + IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); + IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0); + IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_None); // Navigation highlight + IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. + IMGUI_API void RenderMouseCursor(ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); + + // Render helpers (those functions don't access any ImGui state!) + IMGUI_API void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale = 1.0f); + IMGUI_API void RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); + IMGUI_API void RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); + IMGUI_API void RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col); + IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); + IMGUI_API void RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding); + + // Widgets + IMGUI_API void TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0); + IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); + IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0); + IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0); + IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags, float thickness = 1.0f); + IMGUI_API void SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_width); + IMGUI_API bool CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value); + IMGUI_API bool CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value); + + // Widgets: Window Decorations + IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API void Scrollbar(ImGuiAxis axis); + IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 avail_v, ImS64 contents_v, ImDrawFlags flags); + IMGUI_API ImRect GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowResizeCornerID(ImGuiWindow* window, int n); // 0..3: corners + IMGUI_API ImGuiID GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); + + // Widgets low-level behaviors + IMGUI_API bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0); + IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f, ImU32 bg_col = 0); + + // Widgets: Tree Nodes + IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); + IMGUI_API void TreePushOverrideID(ImGuiID id); + IMGUI_API bool TreeNodeGetOpen(ImGuiID storage_id); + IMGUI_API void TreeNodeSetOpen(ImGuiID storage_id, bool open); + IMGUI_API bool TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags); // Return open state. Consume previous SetNextItemOpen() data, if any. May return true when logging. + + // Template functions are instantiated in imgui_widgets.cpp for a finite number of types. + // To use them externally (for custom widget) you may need an "extern template" statement in your code in order to link to existing instances and silence Clang warnings (see #2036). + // e.g. " extern template IMGUI_API float RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, float v); " + template IMGUI_API float ScaleRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API T ScaleValueFromRatioT(ImGuiDataType data_type, float t, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, ImGuiSliderFlags flags); + template IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + template IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v); + template IMGUI_API bool CheckboxFlagsT(const char* label, T* flags, T flags_value); + + // Data type helpers + IMGUI_API const ImGuiDataTypeInfo* DataTypeGetInfo(ImGuiDataType data_type); + IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format); + IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty = NULL); + IMGUI_API int DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max); + + // InputText + IMGUI_API bool InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API void InputTextDeactivateHook(ImGuiID id); + IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags); + IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL); + inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputId == id); } + inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active + IMGUI_API void SetNextItemRefVal(ImGuiDataType data_type, void* p_data); + + // Color + IMGUI_API void ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags); + + // Plot + IMGUI_API int PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg); + + // Shade functions (write over already created vertices) + IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); + IMGUI_API void ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + IMGUI_API void ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out); + + // Garbage collection + IMGUI_API void GcCompactTransientMiscBuffers(); + IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); + IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); + + // Debug Tools + IMGUI_API void DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size); // size >= 0 : alloc, size = -1 : free + IMGUI_API void ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); + IMGUI_API void ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); + IMGUI_API void ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + IMGUI_API void DebugDrawCursorPos(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawLineExtents(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawItemRect(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end); + IMGUI_API void DebugLocateItem(ImGuiID target_id); // Call sparingly: only 1 at the same time! + IMGUI_API void DebugLocateItemOnHover(ImGuiID target_id); // Only call on reaction to a mouse Hover: because only 1 at the same time! + IMGUI_API void DebugLocateItemResolveWithLastItem(); + IMGUI_API void DebugBreakClearData(); + IMGUI_API bool DebugBreakButton(const char* label, const char* description_of_location); + IMGUI_API void DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location); + IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); + IMGUI_API void DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end); + IMGUI_API void DebugNodeColumns(ImGuiOldColumns* columns); + IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label); + IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); + IMGUI_API void DebugNodeFont(ImFont* font); + IMGUI_API void DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); + IMGUI_API void DebugNodeStorage(ImGuiStorage* storage, const char* label); + IMGUI_API void DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); + IMGUI_API void DebugNodeTable(ImGuiTable* table); + IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings); + IMGUI_API void DebugNodeInputTextState(ImGuiInputTextState* state); + IMGUI_API void DebugNodeTypingSelectState(ImGuiTypingSelectState* state); + IMGUI_API void DebugNodeMultiSelectState(ImGuiMultiSelectState* state); + IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); + IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); + IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); + IMGUI_API void DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack); + IMGUI_API void DebugNodeViewport(ImGuiViewportP* viewport); + IMGUI_API void DebugRenderKeyboardPreview(ImDrawList* draw_list); + IMGUI_API void DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb); + + // Obsolete functions +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void SetItemUsingMouseWheel() { SetItemKeyOwner(ImGuiKey_MouseWheelY); } // Changed in 1.89 + inline bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0) { return TreeNodeUpdateNextOpen(id, flags); } // Renamed in 1.89 + + //inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { IM_ASSERT(IsNamedKey(key)); return IsKeyPressed(key, repeat); } // Removed in 1.87: Mapping from named key is always identity! + + // Refactored focus/nav/tabbing system in 1.82 and 1.84. If you have old/custom copy-and-pasted widgets which used FocusableItemRegister(): + // (Old) IMGUI_VERSION_NUM < 18209: using 'ItemAdd(....)' and 'bool tab_focused = FocusableItemRegister(...)' + // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Focused) != 0' + // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))' + //inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() + //inline void FocusableItemUnregister(ImGuiWindow* window) // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem +#endif + +} // namespace ImGui + + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas internal API +//----------------------------------------------------------------------------- + +// This structure is likely to evolve as we add support for incremental atlas updates +struct ImFontBuilderIO +{ + bool (*FontBuilder_Build)(ImFontAtlas* atlas); +}; + +// Helper for font builder +#ifdef IMGUI_ENABLE_STB_TRUETYPE +IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); +#endif +IMGUI_API void ImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); +IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); +IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); +IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); + +//----------------------------------------------------------------------------- +// [SECTION] Test Engine specific hooks (imgui_test_engine) +//----------------------------------------------------------------------------- + +#ifdef IMGUI_ENABLE_TEST_ENGINE +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ctx, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data); // item_data may be NULL +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); +extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const char* fmt, ...); +extern const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ctx, ImGuiID id); + +// In IMGUI_VERSION_NUM >= 18934: changed IMGUI_TEST_ENGINE_ITEM_ADD(bb,id) to IMGUI_TEST_ENGINE_ITEM_ADD(id,bb,item_data); +#define IMGUI_TEST_ENGINE_ITEM_ADD(_ID,_BB,_ITEM_DATA) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemAdd(&g, _ID, _BB, _ITEM_DATA) // Register item bounding box +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS) // Register item label and status flags (optional) +#define IMGUI_TEST_ENGINE_LOG(_FMT,...) if (g.TestEngineHookItems) ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__) // Custom log entry from user land into test log +#else +#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID) ((void)0) +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) ((void)g) +#endif + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop #endif #ifdef _MSC_VER -# pragma warning(pop) +#pragma warning (pop) #endif + +#endif // #ifndef IMGUI_DISABLE From 6793761d519b4bd84c3f4b8d1f734ff559e0f7c3 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 28 Jun 2026 16:25:27 -0700 Subject: [PATCH 16/23] Fix Windows build: include and before OpenXR platform header XR_USE_PLATFORM_WIN32 causes openxr_platform.h to pull in Win32 extension structs that use LARGE_INTEGER and IUnknown. The former is in ; the latter is in which WIN32_LEAN_AND_MEAN excludes from the windows.h umbrella. Previously xr_context.h hardcoded XR_USE_PLATFORM_XLIB (even on Windows), so the Win32 sections were never compiled. Now that the platform define is correct, add the explicit prerequisite includes. --- attachments/openxr_engine/xr_context.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h index 921fce2d6..2cd06884a 100644 --- a/attachments/openxr_engine/xr_context.h +++ b/attachments/openxr_engine/xr_context.h @@ -4,6 +4,11 @@ #include #if defined(_WIN32) #define XR_USE_PLATFORM_WIN32 +// openxr_platform.h with XR_USE_PLATFORM_WIN32 needs LARGE_INTEGER, IUnknown, etc. +// provides the base types; provides IUnknown which is +// excluded by WIN32_LEAN_AND_MEAN (defined globally in the build). +#include +#include #elif defined(__linux__) #define XR_USE_PLATFORM_XLIB #endif From 3ba282880d3036b6653059e25fe680f225d0f5c6 Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 28 Jun 2026 22:10:53 -0700 Subject: [PATCH 17/23] Fix the build by updating imgui dependencies and adding missing tables and widgets files. --- attachments/openxr_engine/CMakeLists.txt | 3 ++- attachments/openxr_engine/camera_component.h | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index 87c290a3c..aa30f28e6 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -148,7 +148,7 @@ set(SOURCES_COMMON ${SIMPLE_ENGINE_DIR}/model_loader.cpp ${SIMPLE_ENGINE_DIR}/audio_system.cpp ${SIMPLE_ENGINE_DIR}/physics_system.cpp - ${SIMPLE_ENGINE_DIR}/imgui_system.cpp + imgui_system.cpp # OpenXR version (ImGui 1.92+ texture lifecycle) ${SIMPLE_ENGINE_DIR}/imgui/imgui.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui_draw.cpp ${SIMPLE_ENGINE_DIR}/imgui/imgui_tables.cpp @@ -187,6 +187,7 @@ target_compile_definitions(OpenXREngine PRIVATE _USE_MATH_DEFINES VULKAN_HPP_NO_STRUCT_CONSTRUCTORS VULKAN_HPP_DISPATCH_LOADER_DYNAMIC + IMGUI_ENABLE_STB_TRUETYPE ) # Link libraries diff --git a/attachments/openxr_engine/camera_component.h b/attachments/openxr_engine/camera_component.h index b2675ce5d..ebdb6832b 100644 --- a/attachments/openxr_engine/camera_component.h +++ b/attachments/openxr_engine/camera_component.h @@ -159,10 +159,10 @@ class CameraComponent : public Component * @param near The near plane distance. * @param far The far plane distance. */ - void SetClipPlanes(float near, float far) + void SetClipPlanes(float nearDist, float farDist) { - nearPlane = near; - farPlane = far; + nearPlane = nearDist; + farPlane = farDist; projectionMatrixDirty = true; } From 032ceb8081a7f0bca6f8eb2e41a43c9c9e545f8e Mon Sep 17 00:00:00 2001 From: swinston Date: Sun, 28 Jun 2026 23:56:02 -0700 Subject: [PATCH 18/23] Add OpenXR session lifecycle handling and ImGui integration. - Implemented OpenXR session state handling to support session start/stop events. - Integrated an ImGui system for rendering custom UI components. - Enhanced XR frame rendering logic to respect session state. - Added logic to dynamically select XR reference spaces based on platform compatibility. - Updated Vulkan rendering logic to handle ImGui frame closures and pipeline binding conditions. - Prevented GTK modules from loading to avoid crashes on Wayland. - Made several fixes, including fallback rendering when the session isn't running and bindings for XR actions and poses. --- attachments/openxr_engine/CMakeLists.txt | 16 +- attachments/openxr_engine/engine.cpp | 31 +- attachments/openxr_engine/imgui_system.cpp | 1126 +++++++++++++++++ attachments/openxr_engine/main.cpp | 12 +- attachments/openxr_engine/renderer.h | 2 + attachments/openxr_engine/renderer_core.cpp | 9 +- .../openxr_engine/renderer_rendering.cpp | 18 +- attachments/openxr_engine/xr_context.cpp | 61 +- attachments/openxr_engine/xr_context.h | 16 +- 9 files changed, 1260 insertions(+), 31 deletions(-) create mode 100644 attachments/openxr_engine/imgui_system.cpp diff --git a/attachments/openxr_engine/CMakeLists.txt b/attachments/openxr_engine/CMakeLists.txt index aa30f28e6..3685d00a0 100644 --- a/attachments/openxr_engine/CMakeLists.txt +++ b/attachments/openxr_engine/CMakeLists.txt @@ -81,15 +81,21 @@ else() endif() # Platform-specific settings +# XR_COMPANION_WINDOW: ON → desktop build with GLFW companion window (default on non-Android) +# OFF → headless XR build for standalone headsets (PICO, embedded Linux, etc.) +option(XR_COMPANION_WINDOW "Enable GLFW companion window for desktop XR mode" ON) + if(ANDROID) - # Android-specific settings add_definitions(-DPLATFORM_ANDROID) find_package(game-activity REQUIRED CONFIG) -else() - # Desktop-specific settings +elseif(XR_COMPANION_WINDOW) add_definitions(-DPLATFORM_DESKTOP) find_package(glfw3 REQUIRED) find_package(OpenAL REQUIRED) +else() + # Headless XR build: no GLFW, no companion window. + add_definitions(-DPLATFORM_HEADSET_ONLY) + find_package(OpenAL QUIET) endif() # Shader compilation (using local shaders in openxr_engine) @@ -201,8 +207,10 @@ target_link_libraries(OpenXREngine PUBLIC if (ANDROID) target_link_libraries(OpenXREngine PUBLIC game-activity::game-activity OpenSLES android log) -else () +elseif(XR_COMPANION_WINDOW) target_link_libraries(OpenXREngine PRIVATE glfw OpenAL::OpenAL) +elseif(OpenAL_FOUND) + target_link_libraries(OpenXREngine PRIVATE OpenAL::OpenAL) endif() # Windows/MSVC portability and build settings diff --git a/attachments/openxr_engine/engine.cpp b/attachments/openxr_engine/engine.cpp index 6c5303e6c..57daeb540 100644 --- a/attachments/openxr_engine/engine.cpp +++ b/attachments/openxr_engine/engine.cpp @@ -17,6 +17,7 @@ #include "engine.h" #include "mesh_component.h" #include "scene_loading.h" +#include "imgui.h" #include #include @@ -24,6 +25,7 @@ #include #include #include +#include // This implementation corresponds to the Engine_Architecture chapter in the tutorial: // @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -151,14 +153,31 @@ void Engine::Run() { if (renderer->IsXrMode()) { auto& xrContext = renderer->GetXrContext(); - auto frameState = xrContext.waitFrame(); - xrContext.beginFrame(); - deltaTimeMs = CalculateDeltaTimeMs(); - Update(frameState.predictedDisplayTime); - Render(frameState.predictedDisplayTime); + // Drive the session state machine; handles xrBeginSession/xrEndSession. + xrContext.pollEvents(); + + if (xrContext.isSessionRunning()) { + auto frameState = xrContext.waitFrame(); + xrContext.beginFrame(); - xrContext.endFrame(renderer->GetXrImageViews()); + deltaTimeMs = CalculateDeltaTimeMs(); + Update(frameState.predictedDisplayTime); + + if (frameState.shouldRender) { + Render(frameState.predictedDisplayTime); + } else { + // Frame submitted but not rendered (occluded/minimised) — close ImGui frame. + if (imguiSystem) ImGui::EndFrame(); + } + + xrContext.endFrame(renderer->GetXrImageViews()); + } else { + // Session not running: yield to avoid busy-spinning and keep watchdog fed. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + renderer->TouchWatchdog(); + deltaTimeMs = std::chrono::milliseconds(10); + } // Update frame counter and FPS for window title (companion window) frameCount++; diff --git a/attachments/openxr_engine/imgui_system.cpp b/attachments/openxr_engine/imgui_system.cpp new file mode 100644 index 000000000..a0da2d068 --- /dev/null +++ b/attachments/openxr_engine/imgui_system.cpp @@ -0,0 +1,1126 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "imgui_system.h" +#include "audio_system.h" +#include "renderer.h" + +// Include ImGui headers +#include "imgui/imgui.h" + +#include + +// This implementation corresponds to the GUI chapter in the tutorial: +// @see en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc + +ImGuiSystem::ImGuiSystem() { + // Constructor implementation +} + +ImGuiSystem::~ImGuiSystem() { + // Destructor implementation + Cleanup(); +} + +bool ImGuiSystem::Initialize(Renderer* renderer, uint32_t width, uint32_t height) { + if (initialized) { + return true; + } + + this->renderer = renderer; + this->width = width; + this->height = height; + + // Create ImGui context + context = ImGui::CreateContext(); + if (!context) { + std::cerr << "Failed to create ImGui context" << std::endl; + return false; + } + + // Configure ImGui + ImGuiIO& io = ImGui::GetIO(); + // Set display size + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Inform ImGui that we support the new texture update protocol (v1.92+) + io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Create Vulkan resources + if (!createResources()) { + std::cerr << "Failed to create ImGui Vulkan resources" << std::endl; + Cleanup(); + return false; + } + + // Initialize per-frame buffers containers + if (renderer) { + uint32_t frames = renderer->GetMaxFramesInFlight(); + vertexBuffers.clear(); + vertexBuffers.reserve(frames); + vertexBufferMemories.clear(); + vertexBufferMemories.reserve(frames); + indexBuffers.clear(); + indexBuffers.reserve(frames); + indexBufferMemories.clear(); + indexBufferMemories.reserve(frames); + for (uint32_t i = 0; i < frames; ++i) { + vertexBuffers.emplace_back(nullptr); + vertexBufferMemories.emplace_back(nullptr); + indexBuffers.emplace_back(nullptr); + indexBufferMemories.emplace_back(nullptr); + } + vertexCounts.assign(frames, 0); + indexCounts.assign(frames, 0); + } + + initialized = true; + return true; +} + +void ImGuiSystem::Cleanup() { + if (!initialized) { + return; + } + + // Wait for the device to be idle before cleaning up + if (renderer) { + renderer->WaitIdle(); + } + // Destroy ImGui context + if (context) { + ImGui::DestroyContext(context); + context = nullptr; + } + + initialized = false; +} + +void ImGuiSystem::SetAudioSystem(AudioSystem* audioSystem) { + this->audioSystem = audioSystem; + + // Load the grass-step-right.wav file and create audio source + if (audioSystem) { + if (audioSystem->LoadAudio("Assets/grass-step-right.wav", "grass_step")) { + audioSource = audioSystem->CreateAudioSource("grass_step"); + if (audioSource) { + audioSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + audioSource->SetVolume(0.8f); + audioSource->SetLoop(true); + std::cout << "Audio source created and configured for HRTF demo" << std::endl; + } + } + + // Also create a debug ping source for testing + debugPingSource = audioSystem->CreateDebugPingSource("debug_ping"); + if (debugPingSource) { + debugPingSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + debugPingSource->SetVolume(0.8f); + debugPingSource->SetLoop(true); + std::cout << "Debug ping source created for audio debugging" << std::endl; + } + } +} + +void ImGuiSystem::NewFrame() { + if (!initialized) { + return; + } + + // Reset the flag at the start of each frame + frameAlreadyRendered = false; + + ImGui::NewFrame(); + + // Loading overlay: show a fullscreen progress bar while the initial scene is loading. + // The bar resets between phases (Textures -> Physics -> AS -> Finalizing) so users + // don't stare at a 100% bar while the engine is still doing work. + if (renderer) { + const bool modelLoading = renderer->IsLoading(); + if (modelLoading) { + ImGuiIO& io = ImGui::GetIO(); + // Suppress right-click while loading (v1.87+ event API) + io.AddMouseButtonEvent(1, false); + + const ImVec2 dispSize = io.DisplaySize; + + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(dispSize); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNav; + + if (ImGui::Begin("##LoadingOverlay", nullptr, flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + // Center the progress elements + const float barWidth = dispSize.x * 0.8f; + const float barX = (dispSize.x - barWidth) * 0.5f; + const float barY = dispSize.y * 0.45f; + ImGui::SetCursorPos(ImVec2(barX, barY)); + ImGui::BeginGroup(); + + // Phase-aware progress (resets between phases). + float frac = 0.0f; + auto phase = renderer->GetLoadingPhase(); + if (phase == Renderer::LoadingPhase::Textures) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + frac = (scheduled > 0) ? (static_cast(completed) / static_cast(scheduled)) : 0.0f; + } else if (phase == Renderer::LoadingPhase::AccelerationStructures) { + frac = renderer->GetASBuildProgress(); + } else { + frac = renderer->GetLoadingPhaseProgress(); + } + ImGui::ProgressBar(frac, ImVec2(barWidth, 0.0f)); + ImGui::Dummy(ImVec2(0.0f, 10.0f)); + ImGui::SetCursorPosX(barX); + ImGui::Text("Loading: %s", renderer->GetLoadingPhaseName()); + if (phase == Renderer::LoadingPhase::Textures) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + ImGui::Text("Textures: %u/%u", completed, scheduled); + } else if (phase == Renderer::LoadingPhase::AccelerationStructures) { + const uint32_t done = renderer->GetASBuildItemsDone(); + const uint32_t total = renderer->GetASBuildItemsTotal(); + ImGui::Text("%s (%u/%u, %.1fs)", renderer->GetASBuildStage(), done, total, renderer->GetASBuildElapsedSeconds()); + } + ImGui::EndGroup(); + ImGui::PopStyleVar(); + } + ImGui::End(); + return; + } + } + + // --- Streaming status: small progress indicator in the upper-right --- + // Once the scene is visible, textures may continue streaming to the GPU. + // Show a compact progress bar in the top-right while there are still + // outstanding texture tasks, and hide it once everything is fully loaded. + if (renderer) { + const uint32_t uploadTotal = renderer->GetUploadJobsTotal(); + const uint32_t uploadDone = renderer->GetUploadJobsCompleted(); + const bool modelLoading = renderer->IsLoading(); + const bool showASBuild = renderer->ShouldShowASBuildProgressInUI(); + + // Acceleration structure build can happen after initial load completes. + // If it takes a long time, show a compact progress window. + if (!modelLoading && showASBuild) { + ImGuiIO& io = ImGui::GetIO(); + const ImVec2 dispSize = io.DisplaySize; + + const float windowWidth = std::min(320.0f, dispSize.x * 0.42f); + const float windowHeight = 90.0f; + const ImVec2 winPos(dispSize.x - windowWidth - 10.0f, 10.0f); + + ImGui::SetNextWindowPos(winPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(windowWidth, windowHeight)); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoSavedSettings; + + if (ImGui::Begin("##ASBuildStatus", nullptr, flags)) { + ImGui::Text("Building acceleration structures..."); + const float asFrac = renderer->GetASBuildProgress(); + ImGui::ProgressBar(asFrac, ImVec2(-1.0f, 0.0f)); + const uint32_t done = renderer->GetASBuildItemsDone(); + const uint32_t total = renderer->GetASBuildItemsTotal(); + ImGui::Text("%s (%u/%u, %.1fs)", + renderer->GetASBuildStage(), + done, + total, + renderer->GetASBuildElapsedSeconds()); + } + ImGui::End(); + } + + if (!modelLoading && uploadTotal > 0 && uploadDone < uploadTotal) { + ImGuiIO& io = ImGui::GetIO(); + const ImVec2 dispSize = io.DisplaySize; + + const float windowWidth = std::min(260.0f, dispSize.x * 0.35f); + const float windowHeight = 120.0f; + // If the AS build status window is visible, offset streaming window below it. + const float yBase = 10.0f + (showASBuild ? (90.0f + 10.0f) : 0.0f); + const ImVec2 winPos(dispSize.x - windowWidth - 10.0f, yBase); + + ImGui::SetNextWindowPos(winPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(windowWidth, windowHeight)); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoCollapse; + + if (ImGui::Begin("##StreamingTextures", nullptr, flags)) { + ImGui::TextUnformatted("Streaming textures to GPU"); + float frac = (uploadTotal > 0) ? (float) uploadDone / (float) uploadTotal : 0.0f; + ImGui::ProgressBar(frac, ImVec2(-1.0f, 0.0f)); + + // Perf counters + const double mbps = renderer->GetUploadThroughputMBps(); + const double avgMs = renderer->GetAverageUploadMs(); + const double totalMB = (double) renderer->GetBytesUploadedTotal() / (1024.0 * 1024.0); + ImGui::Text("Throughput: %.1f MB/s", mbps); + ImGui::SameLine(); + ImGui::Text("Avg upload: %.2f ms/tex", avgMs); + ImGui::Text("Total uploaded: %.1f MB", totalMB); + } + ImGui::End(); + } + } + + // Create HRTF Audio Control UI + ImGui::Begin("HRTF Audio Controls"); + ImGui::Text("3D Audio Position Control"); + + // Audio source selection + ImGui::Separator(); + ImGui::Text("Audio Source Selection:"); + + static bool useDebugPing = false; + if (ImGui::Checkbox("Use Debug Ping (800Hz sine wave)", &useDebugPing)) { + // Stop current audio + if (audioSource && audioSource->IsPlaying()) { + audioSource->Stop(); + } + if (debugPingSource && debugPingSource->IsPlaying()) { + debugPingSource->Stop(); + } + std::cout << "Switched to " << (useDebugPing ? "debug ping" : "file audio") << " source" << std::endl; + } + + // Display current audio source position + ImGui::Text("Audio Source Position: (%.2f, %.2f, %.2f)", audioSourceX, audioSourceY, audioSourceZ); + ImGui::Text("Current Source: %s", useDebugPing ? "Debug Ping (800Hz)" : "grass-step-right.wav"); + + // Directional control buttons + ImGui::Separator(); + ImGui::Text("Directional Controls:"); + + // Get current active source + AudioSource* currentSource = useDebugPing ? debugPingSource : audioSource; + + // Up button + if (ImGui::Button("Up")) { + audioSourceY += 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved up to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Left and Right buttons on same line + if (ImGui::Button("Left")) { + audioSourceX -= 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved left to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("Right")) { + audioSourceX += 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved right to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Down button + if (ImGui::Button("Down")) { + audioSourceY -= 0.5f; + if (currentSource) { + currentSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + } + std::cout << (useDebugPing ? "Debug ping" : "Audio") << " moved down to (" << audioSourceX << ", " << audioSourceY << ", " << audioSourceZ << ")" << std::endl; + } + + // Audio playback controls + ImGui::Separator(); + ImGui::Text("Playback Controls:"); + + // Play button + if (ImGui::Button("Play")) { + if (currentSource) { + currentSource->Play(); + if (audioSystem) { + audioSystem->FlushOutput(); + } + if (useDebugPing) { + std::cout << "Started playing debug ping (800Hz sine wave) with HRTF processing" << std::endl; + } else { + std::cout << "Started playing grass-step-right.wav with HRTF processing" << std::endl; + } + } else { + std::cout << "No audio source available - audio system not initialized" << std::endl; + } + } + ImGui::SameLine(); + + // Stop button + if (ImGui::Button("Stop")) { + if (currentSource) { + currentSource->Stop(); + if (useDebugPing) { + std::cout << "Stopped debug ping playback" << std::endl; + } else { + std::cout << "Stopped audio playback" << std::endl; + } + } + } + + // Additional info + ImGui::Separator(); + if (audioSystem && audioSystem->IsHRTFEnabled()) { + ImGui::Text("HRTF Processing: ENABLED"); + ImGui::Text("Use directional buttons to move the audio source in 3D space"); + ImGui::Text("You should hear the audio move around you!"); + + // HRTF Processing Mode: GPU only (checkbox removed) + ImGui::Separator(); + ImGui::Text("HRTF Processing Mode:"); + ImGui::Text("Current Mode: Vulkan shader processing (GPU)"); + } + else { + ImGui::Text("HRTF Processing: DISABLED"); + } + + // Ball Debugging Controls + ImGui::Separator(); + ImGui::Text("Ball Debugging Controls:"); + + if (ImGui::Checkbox("Ball-Only Rendering", &ballOnlyRenderingEnabled)) { + std::cout << "Ball-only rendering " << (ballOnlyRenderingEnabled ? "enabled" : "disabled") << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("?##BallOnlyHelp")) { + // Help tooltip will be shown on hover + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, only balls will be rendered.\nAll other geometry (bistro scene) will be hidden."); + } + + if (ImGui::Checkbox("Camera Track Ball", &cameraTrackingEnabled)) { + std::cout << "Camera tracking " << (cameraTrackingEnabled ? "enabled" : "disabled") << std::endl; + } + ImGui::SameLine(); + if (ImGui::Button("?##CameraTrackHelp")) { + // Help tooltip will be shown on hover + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, camera will automatically\nfollow and look at the ball."); + } + + // Status display + if (ballOnlyRenderingEnabled) { + ImGui::Text("Status: Only balls are being rendered"); + } else { + ImGui::Text("Status: All geometry is being rendered"); + } + + if (cameraTrackingEnabled) { + ImGui::Text("Camera: Tracking ball automatically"); + } else { + ImGui::Text("Camera: Manual control (WASD + mouse)"); + } + + // Texture loading progress + if (renderer) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + if (scheduled > 0 && completed < scheduled) { + ImGui::Separator(); + float frac = scheduled ? (float) completed / (float) scheduled : 1.0f; + ImGui::Text("Loading textures: %u / %u", completed, scheduled); + ImGui::ProgressBar(frac, ImVec2(-FLT_MIN, 0.0f)); + ImGui::Text("You can continue interacting while textures stream in..."); + } + } + + ImGui::End(); +} + +void ImGuiSystem::Render(vk::raii::CommandBuffer& commandBuffer, uint32_t frameIndex) { + if (!initialized) { + return; + } + + // End the frame and prepare for rendering + ImGui::Render(); + + // Update vertex and index buffers for this frame + updateBuffers(frameIndex); + + // Record rendering commands + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + // Process dynamic texture updates (v1.92+ RendererHasTextures protocol) + if (drawData->Textures) { + for (int n = 0; n < drawData->Textures->Size; n++) { + ImTextureData* tex = (*drawData->Textures)[n]; + if (tex->Status != ImTextureStatus_OK) { + UpdateTexture(tex); + } + } + } + + try { + // Bind the pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); + + // Set viewport + vk::Viewport viewport; + viewport.width = ImGui::GetIO().DisplaySize.x; + viewport.height = ImGui::GetIO().DisplaySize.y; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + commandBuffer.setViewport(0, {viewport}); + + // Set push constants + struct PushConstBlock { + float scale[2]; + float translate[2]; + } pushConstBlock{}; + + pushConstBlock.scale[0] = 2.0f / ImGui::GetIO().DisplaySize.x; + pushConstBlock.scale[1] = 2.0f / ImGui::GetIO().DisplaySize.y; + pushConstBlock.translate[0] = -1.0f; + pushConstBlock.translate[1] = -1.0f; + + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, 0, pushConstBlock); + + // Bind vertex and index buffers for this frame + commandBuffer.bindVertexBuffers(0, *vertexBuffers[frameIndex], vk::DeviceSize{0}); + commandBuffer.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); + + // Render command lists + int vertexOffset = 0; + int indexOffset = 0; + + for (int i = 0; i < drawData->CmdLists.Size; i++) { + const ImDrawList* cmdList = drawData->CmdLists[i]; + + for (int j = 0; j < cmdList->CmdBuffer.Size; j++) { + const ImDrawCmd* pcmd = &cmdList->CmdBuffer[j]; + + // Set scissor rectangle + vk::Rect2D scissor; + scissor.offset.x = std::max(static_cast(pcmd->ClipRect.x), 0); + scissor.offset.y = std::max(static_cast(pcmd->ClipRect.y), 0); + scissor.extent.width = static_cast(pcmd->ClipRect.z - pcmd->ClipRect.x); + scissor.extent.height = static_cast(pcmd->ClipRect.w - pcmd->ClipRect.y); + commandBuffer.setScissor(0, {scissor}); + + // Bind descriptor set (v1.92+ protocol: TexID stores the descriptor set handle or ID) + VkDescriptorSet texHandle = (VkDescriptorSet)pcmd->GetTexID(); + if (texHandle) { + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {vk::DescriptorSet(texHandle)}, {}); + } else { + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pipelineLayout, 0, {*descriptorSet}, {}); + } + + // Draw + commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); + indexOffset += pcmd->ElemCount; + } + + vertexOffset += cmdList->VtxBuffer.Size; + } + } catch (const std::exception& e) { + std::cerr << "Failed to render ImGui: " << e.what() << std::endl; + } +} + +void ImGuiSystem::HandleMouse(float x, float y, uint32_t buttons) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + + // Update mouse position (v1.87+ event API) + io.AddMousePosEvent(x, y); + + // Update mouse buttons (v1.87+ event API) + // We compare with current state to send events only on change + static uint32_t lastButtons = 0; + if ((buttons & 0x01) != (lastButtons & 0x01)) io.AddMouseButtonEvent(0, (buttons & 0x01) != 0); + if ((buttons & 0x02) != (lastButtons & 0x02)) io.AddMouseButtonEvent(1, (buttons & 0x02) != 0); + if ((buttons & 0x04) != (lastButtons & 0x04)) io.AddMouseButtonEvent(2, (buttons & 0x04) != 0); + lastButtons = buttons; +} + +void ImGuiSystem::HandleKeyboard(uint32_t key, bool pressed) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + + // Update key state (v1.87+ event API) + // GLFW key codes are compatible with ImGuiKey values in modern ImGui backends + if (key < 512) { + io.AddKeyEvent((ImGuiKey)key, pressed); + } +} + +void ImGuiSystem::HandleChar(uint32_t c) { + if (!initialized) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + io.AddInputCharacter(c); +} + +void ImGuiSystem::HandleResize(uint32_t width, uint32_t height) { + if (!initialized) { + return; + } + + this->width = width; + this->height = height; + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); +} + +bool ImGuiSystem::WantCaptureKeyboard() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureKeyboard; +} + +bool ImGuiSystem::WantCaptureMouse() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureMouse; +} + +bool ImGuiSystem::createResources() { + // Create all Vulkan resources needed for ImGui rendering + // (Font texture is now handled dynamically via UpdateTexture during Render) + + if (!createDescriptorSetLayout()) { + return false; + } + + if (!createDescriptorPool()) { + return false; + } + + if (!createDescriptorSet()) { + return false; + } + + if (!createPipelineLayout()) { + return false; + } + + if (!createPipeline()) { + return false; + } + + return true; +} + +void ImGuiSystem::UpdateTexture(ImTextureData* tex) { + if (tex->Status == ImTextureStatus_WantDestroy) { + // Release GPU resources and acknowledge so ImGui removes this entry next frame. + fontImage = vk::raii::Image(nullptr); + fontMemory = vk::raii::DeviceMemory(nullptr); + fontView = vk::raii::ImageView(nullptr); + fontSampler = vk::raii::Sampler(nullptr); + tex->SetStatus(ImTextureStatus_Destroyed); + return; + } + + if (tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates) { + int texWidth = tex->Width; + int texHeight = tex->Height; + unsigned char* fontData = (unsigned char*)tex->Pixels; + + if (!fontData) return; + + vk::DeviceSize uploadSize = texWidth * texHeight * tex->BytesPerPixel; + + try { + const vk::raii::Device& device = renderer->GetRaiiDevice(); + + if (tex->Status == ImTextureStatus_WantCreate) { + // Create the font image + vk::ImageCreateInfo imageInfo; + imageInfo.imageType = vk::ImageType::e2D; + imageInfo.format = (tex->BytesPerPixel == 4) ? vk::Format::eR8G8B8A8Unorm : vk::Format::eR8Unorm; + imageInfo.extent.width = static_cast(texWidth); + imageInfo.extent.height = static_cast(texHeight); + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = vk::SampleCountFlagBits::e1; + imageInfo.tiling = vk::ImageTiling::eOptimal; + imageInfo.usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst; + imageInfo.sharingMode = vk::SharingMode::eExclusive; + imageInfo.initialLayout = vk::ImageLayout::eUndefined; + + fontImage = vk::raii::Image(device, imageInfo); + + // Allocate memory for the image + vk::MemoryRequirements memRequirements = fontImage.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal); + + fontMemory = vk::raii::DeviceMemory(device, allocInfo); + fontImage.bindMemory(*fontMemory, 0); + + // Create image view + vk::ImageViewCreateInfo viewInfo; + viewInfo.image = *fontImage; + viewInfo.viewType = vk::ImageViewType::e2D; + viewInfo.format = (tex->BytesPerPixel == 4) ? vk::Format::eR8G8B8A8Unorm : vk::Format::eR8Unorm; + viewInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + fontView = vk::raii::ImageView(device, viewInfo); + + // Create sampler + vk::SamplerCreateInfo samplerInfo; + samplerInfo.magFilter = vk::Filter::eLinear; + samplerInfo.minFilter = vk::Filter::eLinear; + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.anisotropyEnable = VK_FALSE; + samplerInfo.maxAnisotropy = 1.0f; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.compareOp = vk::CompareOp::eAlways; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.0f; + samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; + samplerInfo.unnormalizedCoordinates = VK_FALSE; + + fontSampler = vk::raii::Sampler(device, samplerInfo); + + // Update descriptor set + vk::DescriptorImageInfo imageDescriptor; + imageDescriptor.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageDescriptor.imageView = *fontView; + imageDescriptor.sampler = *fontSampler; + + vk::WriteDescriptorSet writeSet; + writeSet.dstSet = *descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageDescriptor; + writeSet.dstBinding = 0; + + device.updateDescriptorSets({writeSet}, {}); + } + + // Create a staging buffer for uploading the data + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = uploadSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eTransferSrc; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + vk::raii::Buffer stagingBuffer(device, bufferInfo); + vk::MemoryRequirements stagingMemRequirements = stagingBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo stagingAllocInfo; + stagingAllocInfo.allocationSize = stagingMemRequirements.size; + stagingAllocInfo.memoryTypeIndex = renderer->FindMemoryType(stagingMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vk::raii::DeviceMemory stagingBufferMemory(device, stagingAllocInfo); + stagingBuffer.bindMemory(*stagingBufferMemory, 0); + + // Copy data to staging buffer + void* mappedData = stagingBufferMemory.mapMemory(0, uploadSize); + memcpy(mappedData, fontData, uploadSize); + stagingBufferMemory.unmapMemory(); + + // Transition image layout and copy data. + // WantCreate: image is brand-new (Undefined). WantUpdates: image is already in ShaderReadOnlyOptimal. + vk::Format format = (tex->BytesPerPixel == 4) ? vk::Format::eR8G8B8A8Unorm : vk::Format::eR8Unorm; + vk::ImageLayout srcLayout = (tex->Status == ImTextureStatus_WantCreate) + ? vk::ImageLayout::eUndefined + : vk::ImageLayout::eShaderReadOnlyOptimal; + renderer->TransitionImageLayout(*fontImage, + format, + srcLayout, + vk::ImageLayout::eTransferDstOptimal); + renderer->CopyBufferToImage(*stagingBuffer, + *fontImage, + static_cast(texWidth), + static_cast(texHeight)); + renderer->TransitionImageLayout(*fontImage, + format, + vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eShaderReadOnlyOptimal); + + // Store descriptor set handle as the ImTextureID + tex->SetTexID((ImTextureID)(intptr_t)(VkDescriptorSet)*descriptorSet); + tex->SetStatus(ImTextureStatus_OK); + + } catch (const std::exception& e) { + std::cerr << "Failed to update ImGui texture: " << e.what() << std::endl; + } + } +} + +bool ImGuiSystem::createDescriptorSetLayout() { + try { + vk::DescriptorSetLayoutBinding binding; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; + binding.descriptorCount = 1; + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + binding.binding = 0; + + vk::DescriptorSetLayoutCreateInfo layoutInfo; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorPool() { + try { + vk::DescriptorPoolSize poolSize; + poolSize.type = vk::DescriptorType::eCombinedImageSampler; + poolSize.descriptorCount = 1; + + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorSet() { + try { + vk::DescriptorSetAllocateInfo allocInfo; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &(*descriptorSetLayout); + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + vk::raii::DescriptorSets descriptorSets(device, allocInfo); + descriptorSet = std::move(descriptorSets[0]); // Store the first (and only) descriptor set + std::cout << "ImGui created descriptor set with handle: " << *descriptorSet << std::endl; + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createPipelineLayout() { + try { + // Push constant range for the transformation matrix + vk::PushConstantRange pushConstantRange; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(float) * 4; // 2 floats for scale, 2 floats for translate + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &(*descriptorSetLayout); + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create pipeline layout: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createPipeline() { + try { + // Load shaders + vk::raii::ShaderModule shaderModule = renderer->CreateShaderModule("shaders/imgui.spv"); + + // Shader stage creation + vk::PipelineShaderStageCreateInfo vertShaderStageInfo; + vertShaderStageInfo.stage = vk::ShaderStageFlagBits::eVertex; + vertShaderStageInfo.module = *shaderModule; + vertShaderStageInfo.pName = "VSMain"; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo; + fragShaderStageInfo.stage = vk::ShaderStageFlagBits::eFragment; + fragShaderStageInfo.module = *shaderModule; + fragShaderStageInfo.pName = "PSMain"; + + std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; + + // Vertex input + vk::VertexInputBindingDescription bindingDescription; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(ImDrawVert); + bindingDescription.inputRate = vk::VertexInputRate::eVertex; + + std::array attributeDescriptions; + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = vk::Format::eR32G32Sfloat; + attributeDescriptions[0].offset = offsetof(ImDrawVert, pos); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = vk::Format::eR32G32Sfloat; + attributeDescriptions[1].offset = offsetof(ImDrawVert, uv); + + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].location = 2; + attributeDescriptions[2].format = vk::Format::eR8G8B8A8Unorm; + attributeDescriptions[2].offset = offsetof(ImDrawVert, col); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo; + vertexInputInfo.vertexBindingDescriptionCount = 1; + vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; + vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); + vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); + + // Input assembly + vk::PipelineInputAssemblyStateCreateInfo inputAssembly; + inputAssembly.topology = vk::PrimitiveTopology::eTriangleList; + inputAssembly.primitiveRestartEnable = VK_FALSE; + + // Viewport and scissor + vk::PipelineViewportStateCreateInfo viewportState; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + viewportState.pViewports = nullptr; // Dynamic state + viewportState.pScissors = nullptr; // Dynamic state + + // Rasterization + vk::PipelineRasterizationStateCreateInfo rasterizer; + rasterizer.depthClampEnable = VK_FALSE; + rasterizer.rasterizerDiscardEnable = VK_FALSE; + rasterizer.polygonMode = vk::PolygonMode::eFill; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = vk::CullModeFlagBits::eNone; + rasterizer.frontFace = vk::FrontFace::eCounterClockwise; + rasterizer.depthBiasEnable = VK_FALSE; + + // Multisampling + vk::PipelineMultisampleStateCreateInfo multisampling; + multisampling.sampleShadingEnable = VK_FALSE; + multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; + + // Depth and stencil testing + vk::PipelineDepthStencilStateCreateInfo depthStencil; + depthStencil.depthTestEnable = VK_FALSE; + depthStencil.depthWriteEnable = VK_FALSE; + depthStencil.depthCompareOp = vk::CompareOp::eLessOrEqual; + depthStencil.depthBoundsTestEnable = VK_FALSE; + depthStencil.stencilTestEnable = VK_FALSE; + + // Color blending + vk::PipelineColorBlendAttachmentState colorBlendAttachment; + colorBlendAttachment.colorWriteMask = + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; + colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd; + colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eZero; + colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd; + + vk::PipelineColorBlendStateCreateInfo colorBlending; + colorBlending.logicOpEnable = VK_FALSE; + colorBlending.attachmentCount = 1; + colorBlending.pAttachments = &colorBlendAttachment; + + // Dynamic state + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState; + dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); + dynamicState.pDynamicStates = dynamicStates.data(); + + vk::Format depthFormat = renderer->findDepthFormat(); + // Create the graphics pipeline with dynamic rendering + vk::PipelineRenderingCreateInfo renderingInfo; + renderingInfo.colorAttachmentCount = 1; + vk::Format colorFormat = renderer->GetSwapChainImageFormat(); // Get the actual swapchain format + renderingInfo.pColorAttachmentFormats = &colorFormat; + renderingInfo.depthAttachmentFormat = depthFormat; + + vk::GraphicsPipelineCreateInfo pipelineInfo; + pipelineInfo.stageCount = static_cast(shaderStages.size()); + pipelineInfo.pStages = shaderStages.data(); + pipelineInfo.pVertexInputState = &vertexInputInfo; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisampling; + pipelineInfo.pDepthStencilState = &depthStencil; + pipelineInfo.pColorBlendState = &colorBlending; + pipelineInfo.pDynamicState = &dynamicState; + pipelineInfo.layout = *pipelineLayout; + pipelineInfo.pNext = &renderingInfo; + pipelineInfo.basePipelineHandle = nullptr; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + pipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; + return false; + } +} + +void ImGuiSystem::updateBuffers(uint32_t frameIndex) { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + try { + const vk::raii::Device& device = renderer->GetRaiiDevice(); + + // Calculate required buffer sizes + vk::DeviceSize vertexBufferSize = drawData->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize indexBufferSize = drawData->TotalIdxCount * sizeof(ImDrawIdx); + + // Resize buffers if needed for this frame + if (frameIndex >= vertexCounts.size()) + return; // Safety + + if (static_cast(drawData->TotalVtxCount) > vertexCounts[frameIndex]) { + // Clean up old buffer + vertexBuffers[frameIndex] = vk::raii::Buffer(nullptr); + vertexBufferMemories[frameIndex] = vk::raii::DeviceMemory(nullptr); + + // Create new vertex buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = vertexBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + vertexBuffers[frameIndex] = vk::raii::Buffer(device, bufferInfo); + + vk::MemoryRequirements memRequirements = vertexBuffers[frameIndex].getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vertexBufferMemories[frameIndex] = vk::raii::DeviceMemory(device, allocInfo); + vertexBuffers[frameIndex].bindMemory(*vertexBufferMemories[frameIndex], 0); + vertexCounts[frameIndex] = drawData->TotalVtxCount; + } + + if (static_cast(drawData->TotalIdxCount) > indexCounts[frameIndex]) { + // Clean up old buffer + indexBuffers[frameIndex] = vk::raii::Buffer(nullptr); + indexBufferMemories[frameIndex] = vk::raii::DeviceMemory(nullptr); + + // Create new index buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = indexBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eIndexBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + indexBuffers[frameIndex] = vk::raii::Buffer(device, bufferInfo); + + vk::MemoryRequirements memRequirements = indexBuffers[frameIndex].getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + indexBufferMemories[frameIndex] = vk::raii::DeviceMemory(device, allocInfo); + indexBuffers[frameIndex].bindMemory(*indexBufferMemories[frameIndex], 0); + indexCounts[frameIndex] = drawData->TotalIdxCount; + } + + // Upload data to buffers for this frame (only if we have data to upload) + if (drawData->TotalVtxCount > 0 && drawData->TotalIdxCount > 0) { + void* vtxMappedMemory = vertexBufferMemories[frameIndex].mapMemory(0, vertexBufferSize); + void* idxMappedMemory = indexBufferMemories[frameIndex].mapMemory(0, indexBufferSize); + + ImDrawVert* vtxDst = static_cast(vtxMappedMemory); + ImDrawIdx* idxDst = static_cast(idxMappedMemory); + + for (int n = 0; n < drawData->CmdListsCount; n++) { + const ImDrawList* cmdList = drawData->CmdLists[n]; + memcpy(vtxDst, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idxDst, cmdList->IdxBuffer.Data, cmdList->IdxBuffer.Size * sizeof(ImDrawIdx)); + vtxDst += cmdList->VtxBuffer.Size; + idxDst += cmdList->IdxBuffer.Size; + } + + vertexBufferMemories[frameIndex].unmapMemory(); + indexBufferMemories[frameIndex].unmapMemory(); + } + } catch (const std::exception& e) { + std::cerr << "Failed to update buffers: " << e.what() << std::endl; + } +} diff --git a/attachments/openxr_engine/main.cpp b/attachments/openxr_engine/main.cpp index ee8224086..9bd84344c 100644 --- a/attachments/openxr_engine/main.cpp +++ b/attachments/openxr_engine/main.cpp @@ -21,6 +21,7 @@ #include "transform_component.h" #include "xr_context.h" +#include #include #include #include @@ -68,7 +69,7 @@ void SetupScene(Engine *engine) renderer->SetLoadingPhase(Renderer::LoadingPhase::Textures); } std::thread([engine] { - LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + LoadGLTFModel(engine, "Assets/bistro/bistro.gltf"); }).detach(); } @@ -115,6 +116,15 @@ void android_main(android_app *app) */ int main(int, char *[]) { + // Prevent libcanberra-gtk-module from loading into this process. On Wayland, + // libdecor-gtk is loaded as a GLFW window decoration backend; it in turn loads + // libcanberra-gtk-module which tries to spawn a PulseAudio thread the first time + // a GTK event fires. If the nptl thread-stack cache contains any corrupted entry + // (e.g., from Monado shared-memory init), that pa_thread_new call aborts with + // "free(): invalid pointer". We don't use GTK sound themes, so unloading the + // module is correct regardless. + setenv("GTK_MODULES", "", 1); + try { // Enable minidump generation for Release-only crashes (e.g., stack cookie failures / fast-fail). diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index b5a1b6e4a..efa5251fd 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -977,6 +977,8 @@ class Renderer { // OpenXR support bool IsXrMode() const { return xrMode; } XrContext& GetXrContext() { return xrContext; } + vk::Format GetSwapChainImageFormat() const { return swapChainImageFormat; } + void TouchWatchdog() { lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); } private: // Platform diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index 92c4a1127..1b63a82e1 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -166,11 +166,13 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer } } - // Create surface + // Create surface (skipped in headless XR builds — no companion window). +#if !defined(PLATFORM_HEADSET_ONLY) if (!createSurface()) { std::cerr << "Failed to create surface" << std::endl; return false; } +#endif // Pick the physical device (non-XR path — surface is available for present-support queries) if (!xrMode) { @@ -203,17 +205,18 @@ bool Renderer::Initialize(const std::string& appName, bool enableValidationLayer return false; } - // Create swap chain + // Create swap chain + image views (skipped in headless XR builds). +#if !defined(PLATFORM_HEADSET_ONLY) if (!createSwapChain()) { std::cerr << "Failed to create swap chain" << std::endl; return false; } - // Create image views if (!createImageViews()) { std::cerr << "Failed to create image views" << std::endl; return false; } +#endif // Setup dynamic rendering if (!setupDynamicRendering()) { diff --git a/attachments/openxr_engine/renderer_rendering.cpp b/attachments/openxr_engine/renderer_rendering.cpp index 783fed894..06ee8cc0a 100644 --- a/attachments/openxr_engine/renderer_rendering.cpp +++ b/attachments/openxr_engine/renderer_rendering.cpp @@ -1228,13 +1228,19 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca cmd.setScissor(0, xrContext.getScissor(0)); // Draw opaque objects - for (const auto& job : opaqueJobs) { - drawRenderJob(cmd, job, currentFrame, 0, false); + if (!opaqueJobs.empty() && !!*pbrGraphicsPipeline) { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrGraphicsPipeline); + for (const auto& job : opaqueJobs) { + drawRenderJob(cmd, job, currentFrame, 0, false); + } } // Draw transparent objects - for (const auto& job : transparentJobs) { - drawRenderJob(cmd, job, currentFrame, 0, true); + if (!transparentJobs.empty() && !!*pbrBlendGraphicsPipeline) { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrBlendGraphicsPipeline); + for (const auto& job : transparentJobs) { + drawRenderJob(cmd, job, currentFrame, 0, true); + } } cmd.endRendering(); @@ -1265,6 +1271,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); } + // ImGui frame must be closed every frame; XR overlay rendering is not yet implemented. + if (imguiSystem) + ImGui::EndFrame(); + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; } diff --git a/attachments/openxr_engine/xr_context.cpp b/attachments/openxr_engine/xr_context.cpp index 332427033..6d5397f78 100644 --- a/attachments/openxr_engine/xr_context.cpp +++ b/attachments/openxr_engine/xr_context.cpp @@ -138,19 +138,35 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi return false; } - XrReferenceSpaceCreateInfo spaceCreateInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; - spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; - spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; - if (xrCreateReferenceSpace(this->session, &spaceCreateInfo, &this->appSpace) != XR_SUCCESS) { - return false; - } - + // Always initialize views to safe defaults so locateViews can populate them. this->views.resize(2, {XR_TYPE_VIEW}); for (uint32_t i = 0; i < 2; ++i) { this->views[i].pose = {{0,0,0,1}, {0,0,0}}; this->views[i].fov = {-1, 1, 1, -1}; } + // Try STAGE first (physical room-scale origin), fall back to LOCAL (head-locked), + // then VIEW (eye-relative) — Monado's QWERTY driver only supports LOCAL/VIEW. + XrReferenceSpaceCreateInfo spaceCreateInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + const XrReferenceSpaceType spaceFallbacks[] = { + XR_REFERENCE_SPACE_TYPE_STAGE, + XR_REFERENCE_SPACE_TYPE_LOCAL, + XR_REFERENCE_SPACE_TYPE_VIEW, + }; + bool spaceCreated = false; + for (auto spaceType : spaceFallbacks) { + spaceCreateInfo.referenceSpaceType = spaceType; + if (xrCreateReferenceSpace(this->session, &spaceCreateInfo, &this->appSpace) == XR_SUCCESS) { + this->referenceSpaceType = spaceType; + spaceCreated = true; + break; + } + } + if (!spaceCreated) { + return false; + } + XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; std::strncpy(actionSetInfo.actionSetName, "main", XR_MAX_ACTION_SET_NAME_SIZE); std::strncpy(actionSetInfo.localizedActionSetName, "Main Actions", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE); @@ -173,8 +189,8 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi createAction("pose_right", "Right Hand Pose", XR_ACTION_TYPE_POSE_INPUT); createAction("grab_left", "Left Grab", XR_ACTION_TYPE_FLOAT_INPUT); createAction("grab_right", "Right Grab", XR_ACTION_TYPE_FLOAT_INPUT); - createAction("Grab", "Grab", XR_ACTION_TYPE_BOOLEAN_INPUT); - createAction("GrabPose", "Grab Pose", XR_ACTION_TYPE_POSE_INPUT); + createAction("grab", "Grab", XR_ACTION_TYPE_BOOLEAN_INPUT); + createAction("grab_pose", "Grab Pose", XR_ACTION_TYPE_POSE_INPUT); createAction("menu", "Menu Button", XR_ACTION_TYPE_BOOLEAN_INPUT); XrPath khrSimplePath; @@ -189,8 +205,8 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi addBinding("trigger_right", "/user/hand/right/input/select/click"); addBinding("pose_left", "/user/hand/left/input/grip/pose"); addBinding("pose_right", "/user/hand/right/input/grip/pose"); - addBinding("Grab", "/user/hand/right/input/select/click"); - addBinding("GrabPose", "/user/hand/right/input/grip/pose"); + addBinding("grab", "/user/hand/right/input/select/click"); + addBinding("grab_pose", "/user/hand/right/input/grip/pose"); addBinding("menu", "/user/hand/left/input/menu/click"); XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; @@ -204,7 +220,7 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi attachInfo.actionSets = &this->actionSet; xrAttachSessionActionSets(this->session, &attachInfo); - for (const auto& actionName : {"pose_left", "pose_right", "GrabPose"}) { + for (const auto& actionName : {"pose_left", "pose_right", "grab_pose"}) { XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; actionSpaceInfo.action = actions[actionName]; actionSpaceInfo.poseInActionSpace = {{0,0,0,1}, {0,0,0}}; @@ -426,6 +442,27 @@ void XrContext::releaseSwapchainImage() { xrReleaseSwapchainImage(swapchains[0].handle, &ri); } +void XrContext::pollEvents() { + XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER}; + while (xrPollEvent(instance, &event) == XR_SUCCESS) { + if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { + auto* stateEvent = reinterpret_cast(&event); + sessionState = stateEvent->state; + if (stateEvent->state == XR_SESSION_STATE_READY) { + XrSessionBeginInfo bi{XR_TYPE_SESSION_BEGIN_INFO}; + bi.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + xrBeginSession(session, &bi); + } else if (stateEvent->state == XR_SESSION_STATE_STOPPING) { + xrEndSession(session); + } else if (stateEvent->state == XR_SESSION_STATE_EXITING || + stateEvent->state == XR_SESSION_STATE_LOSS_PENDING) { + // Signal caller to exit + } + } + event = {XR_TYPE_EVENT_DATA_BUFFER}; + } +} + XrFrameState XrContext::waitFrame() { XrFrameWaitInfo wi{XR_TYPE_FRAME_WAIT_INFO}; this->frameState = {XR_TYPE_FRAME_STATE}; diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h index 2cd06884a..bc09e615a 100644 --- a/attachments/openxr_engine/xr_context.h +++ b/attachments/openxr_engine/xr_context.h @@ -62,6 +62,15 @@ class XrContext { uint32_t acquireSwapchainImage(); void releaseSwapchainImage(); + // Session lifecycle + void pollEvents(); + bool isSessionRunning() const { + return sessionState == XR_SESSION_STATE_SYNCHRONIZED || + sessionState == XR_SESSION_STATE_VISIBLE || + sessionState == XR_SESSION_STATE_FOCUSED; + } + XrSessionState getSessionState() const { return sessionState; } + // Frame Lifecycle (Chapter 5) XrFrameState waitFrame(); void beginFrame(); @@ -70,7 +79,11 @@ class XrContext { // View & Projection (Chapter 4 & 11) void locateViews(XrTime predictedTime); std::vector getLatestViews() const { return views; } - std::array getLatestViewPoses() const { return {views[0].pose, views[1].pose}; } + std::array getLatestViewPoses() const { + static const XrPosef identity = {{0,0,0,1},{0,0,0}}; + return {views.size() > 0 ? views[0].pose : identity, + views.size() > 1 ? views[1].pose : identity}; + } vk::Viewport getViewport(uint32_t eye) const; vk::Rect2D getScissor(uint32_t eye) const; glm::mat4 getProjectionMatrix(uint32_t eye) const; @@ -125,6 +138,7 @@ class XrContext { XrFrameState frameState; std::vector views; + XrSessionState sessionState = XR_SESSION_STATE_UNKNOWN; // Action system members XrActionSet actionSet; From 3dd788f86427a25ffd6154668a73fd2d191b7b05 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 29 Jun 2026 00:08:05 -0700 Subject: [PATCH 19/23] Add error handling for OpenXR session state transitions and improve frame rendering reliability. --- attachments/openxr_engine/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/attachments/openxr_engine/main.cpp b/attachments/openxr_engine/main.cpp index 9bd84344c..eaee43d56 100644 --- a/attachments/openxr_engine/main.cpp +++ b/attachments/openxr_engine/main.cpp @@ -122,8 +122,10 @@ int main(int, char *[]) // a GTK event fires. If the nptl thread-stack cache contains any corrupted entry // (e.g., from Monado shared-memory init), that pa_thread_new call aborts with // "free(): invalid pointer". We don't use GTK sound themes, so unloading the - // module is correct regardless. + // module is correct regardless. GTK_MODULES is Linux-only; no-op on Windows. +#if !defined(_WIN32) setenv("GTK_MODULES", "", 1); +#endif try { From 414e0bdc1b18fd51683adc77e99985475666aa34 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 30 Jun 2026 12:56:19 -0700 Subject: [PATCH 20/23] Remove XR multiview support, rework Vulkan feature chaining, and improve OpenXR session handling. - Removed XR multiview pipeline configurations and updated layer and swapchain logic for simpler rendering. - Integrated pipelineCreationCacheControl and multiview into Vulkan 1.1+ feature structs, aligning with XR runtime requirements. - Improved frame loop reliability by introducing `isSessionFraming` to handle READY state in Monado. - Added reference space sync to align scene and XR environments post-loading. - Updated memory pool to optimize allocation scanning with `nextFreeHint` and freed-space reuse. - Disabled ImGui integration while addressing heap corruption issues. - Refactored session lifecycle handling to reduce busy-spinning, improve stability, and add debug logging. - Improved the Vulkan platform abstraction to include standalone XR headset support. --- .../openxr_engine/camera_component.cpp | 6 +- attachments/openxr_engine/engine.cpp | 57 ++++- attachments/openxr_engine/engine.h | 5 +- attachments/openxr_engine/main.cpp | 15 +- attachments/openxr_engine/memory_pool.cpp | 62 +++-- attachments/openxr_engine/memory_pool.h | 1 + attachments/openxr_engine/platform.h | 233 ++++++++++++++++++ attachments/openxr_engine/renderer.h | 7 + attachments/openxr_engine/renderer_core.cpp | 52 +++- .../openxr_engine/renderer_pipelines.cpp | 10 +- .../openxr_engine/renderer_rendering.cpp | 82 ++++-- .../openxr_engine/renderer_resources.cpp | 2 +- attachments/openxr_engine/xr_context.cpp | 165 ++++++++++--- attachments/openxr_engine/xr_context.h | 15 +- 14 files changed, 590 insertions(+), 122 deletions(-) create mode 100644 attachments/openxr_engine/platform.h diff --git a/attachments/openxr_engine/camera_component.cpp b/attachments/openxr_engine/camera_component.cpp index 1e6ecc84a..ccb02a55a 100644 --- a/attachments/openxr_engine/camera_component.cpp +++ b/attachments/openxr_engine/camera_component.cpp @@ -132,9 +132,9 @@ inline glm::mat4 getAsymmetricProjection(const XrFovf& fov, float nearZ, float f } void CameraComponent::SetStereoViews(const XrView& left, const XrView& right) { - // 1. Convert OpenXR poses to 4x4 matrices - eyeViewMatrices[0] = xrPoseToMatrix(left.pose); - eyeViewMatrices[1] = xrPoseToMatrix(right.pose); + // 1. Convert OpenXR eye poses (eye-to-world) to view matrices (world-to-eye). + eyeViewMatrices[0] = glm::inverse(xrPoseToMatrix(left.pose)); + eyeViewMatrices[1] = glm::inverse(xrPoseToMatrix(right.pose)); // 2. Build asymmetric projection matrices from FOV tangents eyeProjectionMatrices[0] = getAsymmetricProjection(left.fov, nearPlane, farPlane); diff --git a/attachments/openxr_engine/engine.cpp b/attachments/openxr_engine/engine.cpp index 57daeb540..376c37e41 100644 --- a/attachments/openxr_engine/engine.cpp +++ b/attachments/openxr_engine/engine.cpp @@ -117,9 +117,9 @@ bool Engine::Initialize(const std::string& appName, int width, int height, bool // Physics system via constructor (GPU enabled) physicsSystem = std::make_unique(renderer.get(), true); - // ImGui via constructor, then connect audio system - imguiSystem = std::make_unique(renderer.get(), width, height); - imguiSystem->SetAudioSystem(audioSystem.get()); + // ImGui disabled while investigating heap corruption crash in ImGui::NewFrame(). + // imguiSystem = std::make_unique(renderer.get(), width, height); + // imguiSystem->SetAudioSystem(audioSystem.get()); } catch (const std::exception& e) { std::cerr << "Subsystem initialization failed: " << e.what() << std::endl; return false; @@ -157,23 +157,45 @@ void Engine::Run() { // Drive the session state machine; handles xrBeginSession/xrEndSession. xrContext.pollEvents(); - if (xrContext.isSessionRunning()) { + static int xrWaitLoops = 0; + if (!xrContext.isSessionFraming()) { + if (++xrWaitLoops % 200 == 0) { // log every ~2 seconds + std::cout << "[XR] Waiting for session (state=" << xrContext.getSessionState() << "), loop=" << xrWaitLoops << std::endl; + } + } else { + xrWaitLoops = 0; + } + + static bool frameLoopStarted = false; + if (xrContext.isSessionFraming()) { + if (!frameLoopStarted) { + frameLoopStarted = true; + std::cout << "[XR] Entering frame loop (state=" << xrContext.getSessionState() << ")" << std::endl; + } auto frameState = xrContext.waitFrame(); xrContext.beginFrame(); deltaTimeMs = CalculateDeltaTimeMs(); Update(frameState.predictedDisplayTime); - if (frameState.shouldRender) { + // Only render and submit layers when the session is actually running + // (SYNCHRONIZED/VISIBLE/FOCUSED). In READY state just pump the frame loop + // so the runtime can advance to SYNCHRONIZED. + if (frameState.shouldRender && xrContext.isSessionRunning()) { Render(frameState.predictedDisplayTime); } else { - // Frame submitted but not rendered (occluded/minimised) — close ImGui frame. if (imguiSystem) ImGui::EndFrame(); + renderer->TouchWatchdog(); } + static uint32_t dbgEndFrameCount = 0; + if (++dbgEndFrameCount <= 5 || dbgEndFrameCount % 50 == 0) + std::cout << "[Engine] calling endFrame #" << dbgEndFrameCount + << " shouldRender=" << frameState.shouldRender + << " state=" << xrContext.getSessionState() << std::endl; xrContext.endFrame(renderer->GetXrImageViews()); } else { - // Session not running: yield to avoid busy-spinning and keep watchdog fed. + // Session not yet framing: yield to avoid busy-spinning and keep watchdog fed. std::this_thread::sleep_for(std::chrono::milliseconds(10)); renderer->TouchWatchdog(); deltaTimeMs = std::chrono::milliseconds(10); @@ -508,6 +530,25 @@ void Engine::Update(XrTime predictedTime) { activeCamera->SetStereoViews(views[0], views[1]); } } + + // 4. After scene loading completes, align the XR reference space to the scene camera. + // This runs once: when the renderer transitions out of loading state. + if (!xrSpaceSynced && !renderer->IsLoading() && activeCamera) { + auto* owner = activeCamera->GetOwner(); + auto* tf = owner ? owner->GetComponent() : nullptr; + if (tf) { + const glm::mat4& M = tf->GetModelMatrix(); + glm::quat q = glm::quat_cast(glm::mat3(M)); + glm::vec3 t = glm::vec3(M[3]); + glm::quat qInv = glm::conjugate(q); + glm::vec3 tInv = -(glm::mat3_cast(qInv) * t); + XrPosef inversePose; + inversePose.orientation = {qInv.x, qInv.y, qInv.z, qInv.w}; + inversePose.position = {tInv.x, tInv.y, tInv.z}; + xrContext.updateReferenceSpacePose(inversePose); + xrSpaceSynced = true; + } + } } // Use the standard update with the last calculated delta for simulation consistency @@ -548,7 +589,7 @@ void Engine::Update(TimeDelta deltaTime) { audioSystem->Update(deltaTime); // Update ImGui system - imguiSystem->NewFrame(); + if (imguiSystem) imguiSystem->NewFrame(); // Update camera controls if (activeCamera) { diff --git a/attachments/openxr_engine/engine.h b/attachments/openxr_engine/engine.h index 65567b44f..0740c7556 100644 --- a/attachments/openxr_engine/engine.h +++ b/attachments/openxr_engine/engine.h @@ -243,8 +243,9 @@ class Engine CameraComponent *activeCamera = nullptr; // Engine state - bool initialized = false; - bool running = false; + bool initialized = false; + bool running = false; + bool xrSpaceSynced = false; // true after reference space aligned to scene camera post-load // Delta time calculation // deltaTimeMs: time since last frame in milliseconds (for clarity) diff --git a/attachments/openxr_engine/main.cpp b/attachments/openxr_engine/main.cpp index eaee43d56..465433d0c 100644 --- a/attachments/openxr_engine/main.cpp +++ b/attachments/openxr_engine/main.cpp @@ -116,14 +116,15 @@ void android_main(android_app *app) */ int main(int, char *[]) { - // Prevent libcanberra-gtk-module from loading into this process. On Wayland, - // libdecor-gtk is loaded as a GLFW window decoration backend; it in turn loads - // libcanberra-gtk-module which tries to spawn a PulseAudio thread the first time - // a GTK event fires. If the nptl thread-stack cache contains any corrupted entry - // (e.g., from Monado shared-memory init), that pa_thread_new call aborts with - // "free(): invalid pointer". We don't use GTK sound themes, so unloading the - // module is correct regardless. GTK_MODULES is Linux-only; no-op on Windows. + // Prevent libcanberra-gtk3 from attempting to open a PulseAudio connection. + // On Wayland+libdecor-gtk, GLFW loads libcanberra-gtk-module which calls + // pa_thread_new() on the first GTK event. When Monado's shared-memory init + // has left a corrupted entry in the nptl thread-stack cache, that call aborts + // with "free(): invalid pointer". Using the null canberra driver suppresses + // the PulseAudio path entirely. We don't use sound feedback, so this is safe + // on all platforms. GTK_MODULES is cleared as a belt-and-suspenders guard. #if !defined(_WIN32) + setenv("CANBERRA_DRIVER", "null", 1); setenv("GTK_MODULES", "", 1); #endif diff --git a/attachments/openxr_engine/memory_pool.cpp b/attachments/openxr_engine/memory_pool.cpp index 32c603332..ea8dfbd65 100644 --- a/attachments/openxr_engine/memory_pool.cpp +++ b/attachments/openxr_engine/memory_pool.cpp @@ -242,38 +242,43 @@ std::pair MemoryPool::findSuitableBlock(PoolT const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; const size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); - // Search existing blocks for sufficient free space with proper offset alignment + // Search existing blocks for sufficient free space with proper offset alignment. + // Use nextFreeHint to avoid rescanning already-used space on every allocation. for (const auto& block : poolBlocks) { const vk::DeviceSize unit = config.allocationUnit; const size_t totalUnits = block->freeList.size(); - size_t i = 0; - while (i < totalUnits) { - // Ensure starting unit produces an offset aligned to 'alignment' - vk::DeviceSize startOffset = static_cast(i) * unit; - if ((alignment > 0) && (startOffset % alignment != 0)) { - // Advance i to the next unit that aligns with 'alignment' - const vk::DeviceSize remainder = startOffset % alignment; - const vk::DeviceSize advanceBytes = alignment - remainder; - const size_t advanceUnits = static_cast((advanceBytes + unit - 1) / unit); - i += std::max(advanceUnits, 1); - continue; - } + // Try from the hint first; fall back to 0 if the hint region doesn't fit. + for (int pass = 0; pass < 2; ++pass) { + size_t i = (pass == 0) ? block->nextFreeHint : 0; + const size_t stopAt = (pass == 0) ? totalUnits : block->nextFreeHint; + if (i >= stopAt) continue; + + while (i < stopAt) { + // Ensure starting unit produces an offset aligned to 'alignment' + vk::DeviceSize startOffset = static_cast(i) * unit; + if ((alignment > 0) && (startOffset % alignment != 0)) { + const vk::DeviceSize remainder = startOffset % alignment; + const vk::DeviceSize advanceBytes = alignment - remainder; + const size_t advanceUnits = static_cast((advanceBytes + unit - 1) / unit); + i += std::max(advanceUnits, 1); + continue; + } - // From aligned i, check for consecutive free units - size_t consecutiveFree = 0; - size_t j = i; - while (j < totalUnits && block->freeList[j] && consecutiveFree < requiredUnits) { - ++consecutiveFree; - ++j; - } + // From aligned i, check for consecutive free units + size_t consecutiveFree = 0; + size_t j = i; + while (j < stopAt && block->freeList[j] && consecutiveFree < requiredUnits) { + ++consecutiveFree; + ++j; + } - if (consecutiveFree >= requiredUnits) { - return {block.get(), i}; - } + if (consecutiveFree >= requiredUnits) { + return {block.get(), i}; + } - // Move past the checked range - i = (j > i) ? j : (i + 1); + i = (j > i) ? j : (i + 1); + } } } @@ -308,6 +313,9 @@ std::unique_ptr MemoryPool::allocate(PoolType poolType, for (size_t i = startUnit; i < startUnit + requiredUnits; ++i) { block->freeList[i] = false; } + // Advance the hint past the newly allocated region so the next allocation + // doesn't rescan already-used space. + block->nextFreeHint = startUnit + requiredUnits; // Create allocation info auto allocation = std::make_unique(); @@ -344,6 +352,10 @@ void MemoryPool::deallocate(std::unique_ptr allocation) { for (size_t i = startUnit; i < startUnit + numUnits; ++i) { block->freeList[i] = true; } + // Pull the hint back so freed space can be reused without a full rescan. + if (startUnit < block->nextFreeHint) { + block->nextFreeHint = startUnit; + } block->used -= allocation->size; return; diff --git a/attachments/openxr_engine/memory_pool.h b/attachments/openxr_engine/memory_pool.h index 04dd1f8cc..8e31bcc3d 100644 --- a/attachments/openxr_engine/memory_pool.h +++ b/attachments/openxr_engine/memory_pool.h @@ -72,6 +72,7 @@ class MemoryPool void *mappedPtr; // Mapped pointer (if applicable) std::vector freeList; // Free list for sub-allocations vk::DeviceSize allocationUnit; // Size of each allocation unit + size_t nextFreeHint = 0; // Start scanning from here (updated on alloc/dealloc) }; private: diff --git a/attachments/openxr_engine/platform.h b/attachments/openxr_engine/platform.h new file mode 100644 index 000000000..8aa2df231 --- /dev/null +++ b/attachments/openxr_engine/platform.h @@ -0,0 +1,233 @@ +/* Copyright (c) 2025 Holochip Corporation + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// OpenXR-engine local shadow of simple_engine/platform.h. +// Adds PLATFORM_HEADSET_ONLY support for standalone headsets (PICO, embedded +// Linux XR devices) that do not have a display/window system and therefore +// cannot link against GLFW. All other paths are identical to the upstream file. +#pragma once + +#include +#include +#include + +#if defined(PLATFORM_ANDROID) +# include +# include +# include +# include +# include +# include +# define LOGI(...) ((void) __android_log_print(ANDROID_LOG_INFO, "SimpleEngine", __VA_ARGS__)) +# define LOGW(...) ((void) __android_log_print(ANDROID_LOG_WARN, "SimpleEngine", __VA_ARGS__)) +# define LOGE(...) ((void) __android_log_print(ANDROID_LOG_ERROR, "SimpleEngine", __VA_ARGS__)) +#elif defined(PLATFORM_HEADSET_ONLY) +# include +# define LOGI(...) \ + printf(__VA_ARGS__); \ + printf("\n") +# define LOGW(...) \ + printf(__VA_ARGS__); \ + printf("\n") +# define LOGE(...) \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n") +#else +# define GLFW_INCLUDE_VULKAN +# include +# define LOGI(...) \ + printf(__VA_ARGS__); \ + printf("\n") +# define LOGW(...) \ + printf(__VA_ARGS__); \ + printf("\n") +# define LOGE(...) \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n") +#endif + +class Platform { + public: + Platform() = default; + virtual ~Platform() = default; + + virtual bool Initialize(const std::string& appName, int width, int height) = 0; + virtual void Cleanup() = 0; + virtual bool ProcessEvents() = 0; + virtual bool HasWindowResized() = 0; + virtual int GetWindowWidth() const = 0; + virtual int GetWindowHeight() const = 0; + virtual void GetWindowSize(int* width, int* height) const { + *width = GetWindowWidth(); + *height = GetWindowHeight(); + } + virtual bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) = 0; + virtual void SetResizeCallback(std::function callback) = 0; + virtual void SetMouseCallback(std::function callback) = 0; + virtual void SetKeyboardCallback(std::function callback) = 0; + virtual void SetCharCallback(std::function callback) = 0; + virtual void SetWindowTitle(const std::string& title) = 0; +}; + +#if defined(PLATFORM_ANDROID) +// Full AndroidPlatform definition — copy of simple_engine/platform.h section +class AndroidPlatform : public Platform { + private: + android_app* app = nullptr; + int width = 0; + int height = 0; + bool windowResized = false; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + struct DeviceCapabilities { + int apiLevel = 0; + std::string deviceModel; + std::string deviceManufacturer; + int cpuCores = 0; + int64_t totalMemory = 0; + bool supportsVulkan = false; + bool supportsVulkan11 = false; + bool supportsVulkan12 = false; + std::vector supportedVulkanExtensions; + }; + DeviceCapabilities deviceCapabilities; + bool powerSavingMode = false; + bool multiTouchEnabled = true; + + void DetectDeviceCapabilities(); + void SetupPowerSavingMode(); + void InitializeTouchInput(); + + public: + void EnablePowerSavingMode(bool enable); + bool IsPowerSavingModeEnabled() const { return powerSavingMode; } + void EnableMultiTouch(bool enable) { multiTouchEnabled = enable; } + bool IsMultiTouchEnabled() const { return multiTouchEnabled; } + const DeviceCapabilities& GetDeviceCapabilities() const { return deviceCapabilities; } + + explicit AndroidPlatform(android_app* androidApp); + + bool Initialize(const std::string& appName, int width, int height) override; + void Cleanup() override; + bool ProcessEvents() override; + bool HasWindowResized() override; + int GetWindowWidth() const override { return width; } + int GetWindowHeight() const override { return height; } + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; + void SetResizeCallback(std::function callback) override; + void SetMouseCallback(std::function callback) override; + void SetKeyboardCallback(std::function callback) override; + void SetCharCallback(std::function callback) override; + void SetWindowTitle(const std::string& title) override; + + android_app* GetApp() const { return app; } + AAssetManager* GetAssetManager() const { return app ? app->activity->assetManager : nullptr; } +}; + +#elif defined(PLATFORM_HEADSET_ONLY) + +// HeadlessPlatform: stub platform for standalone XR headsets that have no +// display, window system, or GLFW available. The XR swapchain is used directly. +class HeadlessPlatform final : public Platform { + private: + int width = 0; + int height = 0; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + public: + HeadlessPlatform() = default; + + bool Initialize(const std::string& appName, int w, int h) override { + width = w; height = h; + return true; + } + void Cleanup() override {} + bool ProcessEvents() override { return true; } + bool HasWindowResized() override { return false; } + int GetWindowWidth() const override { return width; } + int GetWindowHeight() const override { return height; } + + // No real surface — caller (Renderer) must skip surface-dependent operations. + bool CreateVulkanSurface(VkInstance, VkSurfaceKHR* surface) override { + *surface = VK_NULL_HANDLE; + return false; // signals "no surface" to the renderer + } + + void SetResizeCallback(std::function cb) override { resizeCallback = std::move(cb); } + void SetMouseCallback(std::function cb) override { mouseCallback = std::move(cb); } + void SetKeyboardCallback(std::function cb) override { keyboardCallback = std::move(cb); } + void SetCharCallback(std::function cb) override { charCallback = std::move(cb); } + void SetWindowTitle(const std::string&) override {} + + void SetSize(int w, int h) { width = w; height = h; } +}; + +#else + +// DesktopPlatform: full GLFW-based windowed platform for desktop+XR companion window. +class DesktopPlatform final : public Platform { + private: + GLFWwindow* window = nullptr; + int width = 0; + int height = 0; + bool windowResized = false; + std::function resizeCallback; + std::function mouseCallback; + std::function keyboardCallback; + std::function charCallback; + + static void WindowResizeCallback(GLFWwindow* window, int width, int height); + static void MousePositionCallback(GLFWwindow* window, double xpos, double ypos); + static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods); + static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); + static void CharCallback(GLFWwindow* window, unsigned int codepoint); + + public: + DesktopPlatform() = default; + + bool Initialize(const std::string& appName, int width, int height) override; + void Cleanup() override; + bool ProcessEvents() override; + bool HasWindowResized() override; + int GetWindowWidth() const override { return width; } + int GetWindowHeight() const override { return height; } + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; + void SetResizeCallback(std::function callback) override; + void SetMouseCallback(std::function callback) override; + void SetKeyboardCallback(std::function callback) override; + void SetCharCallback(std::function callback) override; + void SetWindowTitle(const std::string& title) override; + GLFWwindow* GetWindow() const { return window; } +}; +#endif + +template +std::unique_ptr CreatePlatform(Args&&... args) { +#if defined(PLATFORM_ANDROID) + return std::make_unique(std::forward(args)...); +#elif defined(PLATFORM_HEADSET_ONLY) + return std::make_unique(); +#else + return std::make_unique(); +#endif +} diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index efa5251fd..b9d781e8e 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -979,6 +979,13 @@ class Renderer { XrContext& GetXrContext() { return xrContext; } vk::Format GetSwapChainImageFormat() const { return swapChainImageFormat; } void TouchWatchdog() { lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); } + // Process pending GPU uploads and entity preallocations without rendering a frame. + // Call this during XR frame-loop iterations that don't render (e.g., READY state) + // so work is spread across frames instead of burst-processed on the first render frame. + void PumpPendingWork() { + ProcessPendingMeshUploads(); + ProcessPendingEntityPreallocations(); + } private: // Platform diff --git a/attachments/openxr_engine/renderer_core.cpp b/attachments/openxr_engine/renderer_core.cpp index 1b63a82e1..ef5d27128 100644 --- a/attachments/openxr_engine/renderer_core.cpp +++ b/attachments/openxr_engine/renderer_core.cpp @@ -967,16 +967,10 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { vk::PhysicalDeviceTimelineSemaphoreFeatures timelineSemaphoreFeatures; timelineSemaphoreFeatures.timelineSemaphore = vk::True; - // NEW: Enable Multiview if in XR mode (Chapter 8) - vk::PhysicalDeviceMultiviewFeatures multiviewFeatures; - multiviewFeatures.multiview = xrMode ? vk::True : vk::False; - multiviewFeatures.pNext = &timelineSemaphoreFeatures; - // Vulkan memory model features (required for some shader operations) vk::PhysicalDeviceVulkanMemoryModelFeatures memoryModelFeatures; memoryModelFeatures.vulkanMemoryModel = vk::True; memoryModelFeatures.vulkanMemoryModelDeviceScope = memoryModelSupported.vulkanMemoryModelDeviceScope ? vk::True : vk::False; - memoryModelFeatures.pNext = &multiviewFeatures; // Buffer device address features (required for some buffer operations) vk::PhysicalDeviceBufferDeviceAddressFeatures bufferDeviceAddressFeatures; @@ -990,10 +984,15 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { vk::PhysicalDeviceVulkan13Features vulkan13Features; vulkan13Features.dynamicRendering = vk::True; vulkan13Features.synchronization2 = vk::True; + // pipelineCreationCacheControl is required by XR_KHR_vulkan_enable2 runtime pipeline cache usage + vulkan13Features.pipelineCreationCacheControl = vulkan13Supported.pipelineCreationCacheControl ? vk::True : vk::False; - // Vulkan 1.1 features: shaderDrawParameters to satisfy SPIR-V DrawParameters capability + // Vulkan 1.1 features: shaderDrawParameters + multiview for XR stereo rendering. + // Must NOT use a separate VkPhysicalDeviceMultiviewFeatures alongside this struct — + // VUID-VkDeviceCreateInfo-pNext-02829 forbids both in the same pNext chain. vk::PhysicalDeviceVulkan11Features vulkan11Features{}; vulkan11Features.shaderDrawParameters = vk::True; + vulkan11Features.multiview = (xrMode && vulkan11Supported.multiview) ? vk::True : vk::False; // Query extended feature support #if !defined(PLATFORM_ANDROID) auto featureChain = physicalDevice.getFeatures2< @@ -1112,8 +1111,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { // Chain the feature structures together (build pNext chain explicitly) // Base - features.pNext = &multiviewFeatures; - multiviewFeatures.pNext = &timelineSemaphoreFeatures; + features.pNext = &timelineSemaphoreFeatures; timelineSemaphoreFeatures.pNext = &memoryModelFeatures; memoryModelFeatures.pNext = &bufferDeviceAddressFeatures; bufferDeviceAddressFeatures.pNext = &storage8BitFeatures; @@ -1190,8 +1188,40 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) { .pEnabledFeatures = nullptr // Using pNext for features }; - // Create the logical device - device = vk::raii::Device(physicalDevice, createInfo); + // Create the logical device. + // In XR mode use xrCreateVulkanDeviceKHR (XR_KHR_vulkan_enable2) so that Monado's + // IPC compositor is initialised with the full vtable (slot 0x8d8). Without this, + // Monado allocates a reduced compositor struct that lacks the per-layer submission + // function pointer and crashes in xrEndFrame. + if (xrMode) { + PFN_xrCreateVulkanDeviceKHR pfnXrCreateVulkanDevice = nullptr; + xrGetInstanceProcAddr(xrContext.getXrInstance(), "xrCreateVulkanDeviceKHR", + reinterpret_cast(&pfnXrCreateVulkanDevice)); + if (pfnXrCreateVulkanDevice) { + const VkDeviceCreateInfo* vkCreateInfo = reinterpret_cast(&createInfo); + XrVulkanDeviceCreateInfoKHR xrDeviceCreateInfo{XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR}; + xrDeviceCreateInfo.systemId = xrContext.getSystemId(); + xrDeviceCreateInfo.pfnGetInstanceProcAddr = vkGetInstanceProcAddr; + xrDeviceCreateInfo.vulkanCreateInfo = vkCreateInfo; + xrDeviceCreateInfo.vulkanPhysicalDevice = static_cast(*physicalDevice); + xrDeviceCreateInfo.vulkanAllocator = nullptr; + VkDevice vkDev = VK_NULL_HANDLE; + VkResult vkResult = VK_SUCCESS; + XrResult xrResult = pfnXrCreateVulkanDevice(xrContext.getXrInstance(), &xrDeviceCreateInfo, &vkDev, &vkResult); + if (xrResult == XR_SUCCESS && vkResult == VK_SUCCESS) { + device = vk::raii::Device(physicalDevice, vkDev); + } else { + std::cerr << "[XR] xrCreateVulkanDeviceKHR failed (xr=" << xrResult << " vk=" << vkResult + << "), falling back to standard device creation\n"; + device = vk::raii::Device(physicalDevice, createInfo); + } + } else { + std::cerr << "[XR] xrCreateVulkanDeviceKHR not available, falling back to standard device creation\n"; + device = vk::raii::Device(physicalDevice, createInfo); + } + } else { + device = vk::raii::Device(physicalDevice, createInfo); + } // After logical device is created, we can initialize the OpenXR session if (xrMode) { diff --git a/attachments/openxr_engine/renderer_pipelines.cpp b/attachments/openxr_engine/renderer_pipelines.cpp index e9dccc9b1..b301b6c58 100644 --- a/attachments/openxr_engine/renderer_pipelines.cpp +++ b/attachments/openxr_engine/renderer_pipelines.cpp @@ -378,7 +378,7 @@ bool Renderer::createGraphicsPipeline() { // Initialize member variable for proper lifetime management mainPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, + .viewMask = 0x0u, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, .depthAttachmentFormat = depthFormat, @@ -573,7 +573,7 @@ bool Renderer::createPBRPipeline() { // Initialize member variable for proper lifetime management pbrPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, + .viewMask = 0x0u, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, .depthAttachmentFormat = depthFormat, @@ -806,7 +806,7 @@ bool Renderer::createCompositePipeline() { // Dynamic rendering info compositePipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, + .viewMask = 0x0u, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, .depthAttachmentFormat = vk::Format::eUndefined, @@ -931,7 +931,7 @@ bool Renderer::createDepthPrepassPipeline() { vk::Format depthFormat = findDepthFormat(); vk::PipelineRenderingCreateInfo renderingInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, + .viewMask = 0x0u, .colorAttachmentCount = 0, .pColorAttachmentFormats = nullptr, .depthAttachmentFormat = depthFormat @@ -1086,7 +1086,7 @@ bool Renderer::createLightingPipeline() { // Initialize member variable for proper lifetime management lightingPipelineRenderingCreateInfo = vk::PipelineRenderingCreateInfo{ - .viewMask = xrMode ? 0x3u : 0x0u, + .viewMask = 0x0u, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainImageFormat, .depthAttachmentFormat = depthFormat, diff --git a/attachments/openxr_engine/renderer_rendering.cpp b/attachments/openxr_engine/renderer_rendering.cpp index 06ee8cc0a..fc8f14973 100644 --- a/attachments/openxr_engine/renderer_rendering.cpp +++ b/attachments/openxr_engine/renderer_rendering.cpp @@ -120,9 +120,8 @@ bool Renderer::createSwapChain() { swapChainImageFormat = xrContext.getSwapchainFormat(); swapChainExtent = xrContext.getSwapchainExtent(); - // Use a single swapchain with 2 layers for multiview - eyeSwapchainImages[0] = xrContext.enumerateSwapchainImages(); - eyeSwapchainImages[1].clear(); // Not used in multiview mode + eyeSwapchainImages[0] = xrContext.enumerateSwapchainImages(0); + eyeSwapchainImages[1] = xrContext.enumerateSwapchainImages(1); // We still use swapChainImages as a dummy or to track common state if needed return true; @@ -501,11 +500,11 @@ void Renderer::renderReflectionPass(vk::raii::CommandBuffer& cmd, bool Renderer::createImageViews() { try { if (xrMode) { - eyeSwapchainImageViews[0].clear(); - eyeSwapchainImageViews[1].clear(); - for (size_t i = 0; i < eyeSwapchainImages[0].size(); ++i) { - vk::Image img = eyeSwapchainImages[0][i]; - eyeSwapchainImageViews[0].emplace_back(createImageView(img, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1, 2)); // 2 layers for multiview + for (uint32_t eye = 0; eye < 2; ++eye) { + eyeSwapchainImageViews[eye].clear(); + for (vk::Image img : eyeSwapchainImages[eye]) { + eyeSwapchainImageViews[eye].emplace_back(createImageView(img, swapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1, 1)); + } } return true; } @@ -571,7 +570,7 @@ bool Renderer::setupDynamicRendering() { renderingInfo = vk::RenderingInfo{ .renderArea = vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent), .layerCount = 1, - .viewMask = xrMode ? 0x3u : 0x0u, // 0x3 enables views 0 and 1 for multiview + .viewMask = 0x0u, .colorAttachmentCount = static_cast(colorAttachments.size()), .pColorAttachments = colorAttachments.data(), .pDepthAttachment = &depthAttachment @@ -979,7 +978,8 @@ void Renderer::drawRenderJob(const vk::raii::CommandBuffer& cmd, const RenderJob // In multiview, we only need one set per frame as both views share the same UBO uint32_t setIndex = currentFrame; - // 2. Update UBO using the current template (which has correct multiview matrices if in XR) + // 2. Update UBO: stamp per-entity model matrix into the frame template, then flush + frameUboTemplate.model = job.transformComp ? job.transformComp->GetModelMatrix() : glm::mat4(1.0f); updateUniformBufferInternal(setIndex, job.entity, entityRes, nullptr, frameUboTemplate); // 3. Bind geometry @@ -1120,6 +1120,18 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca if (memoryPool) memoryPool->setRenderingActive(true); + // Extract lights into the per-frame storage buffer (mirrors the non-XR path). + if (!staticLights.empty()) { + std::vector lightsSubset; + lightsSubset.reserve(std::min(staticLights.size(), static_cast(MAX_ACTIVE_LIGHTS))); + for (const auto& L : staticLights) { + lightsSubset.push_back(L); + if (lightsSubset.size() >= MAX_ACTIVE_LIGHTS) break; + } + lastFrameLightCount = static_cast(lightsSubset.size()); + updateLightStorageBuffer(currentFrame, lightsSubset, camera); + } + // Prepare frame-constant UBO data once for both eyes // We'll update the eye-specific view/proj matrices inside the eye loop prepareFrameUboTemplate(camera); @@ -1131,9 +1143,11 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca } device.resetFences(*inFlightFences[currentFrame]); - // Execute any pending GPU uploads/preallocations + // Execute any pending GPU uploads/preallocations, then flush newly-uploaded + // textures into descriptor sets for this frame slot. ProcessPendingMeshUploads(); ProcessPendingEntityPreallocations(); + ProcessDirtyDescriptorsForFrame(currentFrame); // Preparation pass: Culling and classification // In a real XR engine, we might do this per eye, but for a simple engine, @@ -1150,12 +1164,39 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca if (entityIt == entityResources.end()) continue; EntityResources& entityRes = entityIt->second; + + // Skip entities whose GPU resources aren't ready yet + auto meshResIt = meshResources.find(meshComponent); + if (meshResIt == meshResources.end()) continue; + MeshResources& meshRes = meshResIt->second; + if (!*meshRes.vertexBuffer || !*meshRes.indexBuffer) continue; + if (entityRes.pbrDescriptorSets.empty()) continue; + ensureEntityMaterialCache(entity, entityRes); + // Initialize descriptor set bindings for this frame slot if not done yet. + // Without this the GPU receives uninitialized UBO/sampler descriptors. + { + std::string texPath = meshComponent->GetBaseColorTexturePath(); + if (texPath.empty()) texPath = meshComponent->GetTexturePath(); + if (!entityRes.pbrUboBindingWritten[currentFrame]) + updateDescriptorSetsForFrame(entity, entityRes, texPath, true, currentFrame, false, true); + if (!entityRes.basicUboBindingWritten[currentFrame]) + updateDescriptorSetsForFrame(entity, entityRes, texPath, false, currentFrame, false, true); + if (!entityRes.pbrImagesWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, true, currentFrame, true, false); + entityRes.pbrImagesWritten[currentFrame] = true; + } + if (!entityRes.basicImagesWritten[currentFrame]) { + updateDescriptorSetsForFrame(entity, entityRes, texPath, false, currentFrame, true, false); + entityRes.basicImagesWritten[currentFrame] = true; + } + } + RenderJob job{ .entity = entity, .entityRes = &entityRes, - .meshRes = &meshResources[meshComponent], + .meshRes = &meshRes, .meshComp = meshComponent, .transformComp = entity->GetComponent(), .isAlphaMasked = entityRes.cachedIsBlended @@ -1181,7 +1222,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca // 2. Transition image to COLOR_ATTACHMENT_OPTIMAL transitionImageLayout(*cmd, swapchainImage, swapChainImageFormat, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, - 1, 2); // 2 layers + 1, 1); // 3. Prepare Multiview UBO Template for (uint32_t eye = 0; eye < 2; ++eye) { @@ -1200,7 +1241,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, - .clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.1f, 0.1f, 0.1f, 1.0f}}} + .clearValue = vk::ClearValue{vk::ClearColorValue{std::array{0.1f, 0.1f, 0.1f, 1.0f}}} }; // Note: depth buffer must be 2 layers for multiview @@ -1215,7 +1256,7 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca vk::RenderingInfo rinfo{ .renderArea = vk::Rect2D({0, 0}, xrContext.getSwapchainExtent()), .layerCount = 1, - .viewMask = 0x3u, // Enable view 0 and 1 + .viewMask = 0x0u, // Single-eye rendering; both XR swapchains get the same image .colorAttachmentCount = 1, .pColorAttachments = &colorAtt, .pDepthAttachment = &depthAtt @@ -1245,13 +1286,6 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca cmd.endRendering(); - // 5. Transition back (OpenXR often expects this or TransferSrcOptimal for blitting) - transitionImageLayout(*cmd, swapchainImage, swapChainImageFormat, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, - 1, 2); - - // 6. Release OpenXR swapchain image - xrContext.releaseSwapchainImage(); - cmd.end(); // --- Submit to Graphics Queue --- @@ -1271,6 +1305,10 @@ void Renderer::Render(const std::vector& entities, CameraComponent* ca graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); } + // Release the OpenXR swapchain image AFTER submitting rendering work. + // The runtime (Monado) will synchronize with the GPU before compositing. + xrContext.releaseSwapchainImage(); + // ImGui frame must be closed every frame; XR overlay rendering is not yet implemented. if (imguiSystem) ImGui::EndFrame(); diff --git a/attachments/openxr_engine/renderer_resources.cpp b/attachments/openxr_engine/renderer_resources.cpp index 6e5fed666..1248b520f 100644 --- a/attachments/openxr_engine/renderer_resources.cpp +++ b/attachments/openxr_engine/renderer_resources.cpp @@ -48,7 +48,7 @@ bool Renderer::createDepthResources() { try { // Find depth format vk::Format depthFormat = findDepthFormat(); - uint32_t layers = xrMode ? 2 : 1; + uint32_t layers = 1; // Use non-pooled createImage for depth as memoryPool doesn't support layers yet auto [image, memory] = createImage( diff --git a/attachments/openxr_engine/xr_context.cpp b/attachments/openxr_engine/xr_context.cpp index 6d5397f78..510048db6 100644 --- a/attachments/openxr_engine/xr_context.cpp +++ b/attachments/openxr_engine/xr_context.cpp @@ -148,7 +148,7 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi // Try STAGE first (physical room-scale origin), fall back to LOCAL (head-locked), // then VIEW (eye-relative) — Monado's QWERTY driver only supports LOCAL/VIEW. XrReferenceSpaceCreateInfo spaceCreateInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; - spaceCreateInfo.poseInReferenceSpace = {{0,0,0,1}, {0,0,0}}; + spaceCreateInfo.poseInReferenceSpace = {{0, 0, 0, 1}, {0, 0, 0}}; const XrReferenceSpaceType spaceFallbacks[] = { XR_REFERENCE_SPACE_TYPE_STAGE, XR_REFERENCE_SPACE_TYPE_LOCAL, @@ -239,6 +239,19 @@ bool XrContext::createSession(vk::PhysicalDevice physicalDevice, vk::Device devi return true; } +void XrContext::updateReferenceSpacePose(const XrPosef& pose) { + if (session == XR_NULL_HANDLE) return; + XrSpace newSpace = XR_NULL_HANDLE; + XrReferenceSpaceCreateInfo ci{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + ci.referenceSpaceType = referenceSpaceType; + ci.poseInReferenceSpace = pose; + if (xrCreateReferenceSpace(session, &ci, &newSpace) == XR_SUCCESS) { + if (appSpace != XR_NULL_HANDLE) + xrDestroySpace(appSpace); + appSpace = newSpace; + } +} + void XrContext::cleanup() { for (auto& swapchain : swapchains) { xrDestroySwapchain(swapchain.handle); @@ -388,7 +401,7 @@ void XrContext::createSwapchains(vk::Device device, vk::Format format, vk::Exten this->extent = extent; XrSwapchainCreateInfo ci{XR_TYPE_SWAPCHAIN_CREATE_INFO}; - ci.arraySize = 2; + ci.arraySize = 1; ci.format = (int64_t)format; ci.width = extent.width; ci.height = extent.height; @@ -397,24 +410,30 @@ void XrContext::createSwapchains(vk::Device device, vk::Format format, vk::Exten ci.sampleCount = 1; ci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; - XrSwapchain handle; - xrCreateSwapchain(this->session, &ci, &handle); + for (uint32_t eye = 0; eye < 2; ++eye) { + XrSwapchain handle; + XrResult r = xrCreateSwapchain(this->session, &ci, &handle); + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrCreateSwapchain eye=" << eye << " failed: " << r << std::endl; + continue; + } - uint32_t count = 0; - xrEnumerateSwapchainImages(handle, 0, &count, nullptr); - std::vector images(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); - xrEnumerateSwapchainImages(handle, count, &count, (XrSwapchainImageBaseHeader*)images.data()); - - SwapchainData data; - data.handle = handle; - data.images = std::move(images); - this->swapchains.push_back(std::move(data)); + uint32_t count = 0; + xrEnumerateSwapchainImages(handle, 0, &count, nullptr); + std::vector images(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); + xrEnumerateSwapchainImages(handle, count, &count, (XrSwapchainImageBaseHeader*)images.data()); + + SwapchainData data; + data.handle = handle; + data.images = std::move(images); + this->swapchains.push_back(std::move(data)); + } } -std::vector XrContext::enumerateSwapchainImages() { +std::vector XrContext::enumerateSwapchainImages(uint32_t swapchainIdx) { std::vector vkImages; - if (!swapchains.empty()) { - for (const auto& img : swapchains[0].images) { + if (swapchainIdx < swapchains.size()) { + for (const auto& img : swapchains[swapchainIdx].images) { vkImages.push_back(vk::Image(img.image)); } } @@ -422,24 +441,51 @@ std::vector XrContext::enumerateSwapchainImages() { } void XrContext::waitSwapchainImage() { - if (swapchains.empty()) return; XrSwapchainImageWaitInfo wi{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; wi.timeout = XR_INFINITE_DURATION; - xrWaitSwapchainImage(swapchains[0].handle, &wi); + for (auto& sc : swapchains) { + XrResult r = xrWaitSwapchainImage(sc.handle, &wi); + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrWaitSwapchainImage failed: " << r << std::endl; + } + } } uint32_t XrContext::acquireSwapchainImage() { if (swapchains.empty()) return 0; XrSwapchainImageAcquireInfo ai{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; uint32_t index = 0; - xrAcquireSwapchainImage(swapchains[0].handle, &ai, &index); + XrResult r = xrAcquireSwapchainImage(swapchains[0].handle, &ai, &index); + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrAcquireSwapchainImage[0] failed: " << r << std::endl; + } + for (uint32_t i = 1; i < swapchains.size(); ++i) { + uint32_t dummy = 0; + xrAcquireSwapchainImage(swapchains[i].handle, &ai, &dummy); + } return index; } void XrContext::releaseSwapchainImage() { - if (swapchains.empty()) return; XrSwapchainImageReleaseInfo ri{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - xrReleaseSwapchainImage(swapchains[0].handle, &ri); + for (auto& sc : swapchains) { + xrReleaseSwapchainImage(sc.handle, &ri); + } +} + +static const char* xrSessionStateName(XrSessionState s) { + switch (s) { + case XR_SESSION_STATE_UNKNOWN: return "UNKNOWN"; + case XR_SESSION_STATE_IDLE: return "IDLE"; + case XR_SESSION_STATE_READY: return "READY"; + case XR_SESSION_STATE_SYNCHRONIZED: return "SYNCHRONIZED"; + case XR_SESSION_STATE_VISIBLE: return "VISIBLE"; + case XR_SESSION_STATE_FOCUSED: return "FOCUSED"; + case XR_SESSION_STATE_STOPPING: return "STOPPING"; + case XR_SESSION_STATE_LOSS_PENDING: return "LOSS_PENDING"; + case XR_SESSION_STATE_EXITING: return "EXITING"; + default: return "?"; + } } void XrContext::pollEvents() { @@ -447,13 +493,22 @@ void XrContext::pollEvents() { while (xrPollEvent(instance, &event) == XR_SUCCESS) { if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { auto* stateEvent = reinterpret_cast(&event); + std::cout << "[XR] Session state: " << xrSessionStateName(sessionState) + << " -> " << xrSessionStateName(stateEvent->state) << std::endl; sessionState = stateEvent->state; if (stateEvent->state == XR_SESSION_STATE_READY) { XrSessionBeginInfo bi{XR_TYPE_SESSION_BEGIN_INFO}; bi.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; - xrBeginSession(session, &bi); + XrResult r = xrBeginSession(session, &bi); + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrBeginSession failed: " << r << std::endl; + } else { + std::cout << "[XR] xrBeginSession succeeded" << std::endl; + sessionBegun = true; + } } else if (stateEvent->state == XR_SESSION_STATE_STOPPING) { xrEndSession(session); + sessionBegun = false; } else if (stateEvent->state == XR_SESSION_STATE_EXITING || stateEvent->state == XR_SESSION_STATE_LOSS_PENDING) { // Signal caller to exit @@ -466,13 +521,24 @@ void XrContext::pollEvents() { XrFrameState XrContext::waitFrame() { XrFrameWaitInfo wi{XR_TYPE_FRAME_WAIT_INFO}; this->frameState = {XR_TYPE_FRAME_STATE}; - xrWaitFrame(this->session, &wi, &this->frameState); + XrResult r = xrWaitFrame(this->session, &wi, &this->frameState); + static uint32_t waitFrameCount = 0; + if (++waitFrameCount <= 5 || waitFrameCount % 100 == 0) { + std::cout << "[XR] xrWaitFrame #" << waitFrameCount + << " result=" << r + << " shouldRender=" << this->frameState.shouldRender + << " state=" << sessionState << std::endl; + } return this->frameState; } void XrContext::beginFrame() { XrFrameBeginInfo bi{XR_TYPE_FRAME_BEGIN_INFO}; - xrBeginFrame(this->session, &bi); + XrResult r = xrBeginFrame(this->session, &bi); + static uint32_t beginFrameCount = 0; + if (++beginFrameCount <= 5 || beginFrameCount % 100 == 0) { + std::cout << "[XR] xrBeginFrame #" << beginFrameCount << " result=" << r << std::endl; + } } void XrContext::endFrame(const std::array, 2>& eyeViews) { @@ -484,28 +550,40 @@ void XrContext::endFrame(const std::array, 2>& eyeVie layer.space = this->appSpace; static XrCompositionLayerProjectionView projectionViews[2]; - for (uint32_t i = 0; i < 2; ++i) { - projectionViews[i] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}; - projectionViews[i].pose = this->views[i].pose; - projectionViews[i].fov = this->views[i].fov; - projectionViews[i].subImage.swapchain = this->swapchains[0].handle; - projectionViews[i].subImage.imageRect = {{0, 0}, {(int32_t)extent.width, (int32_t)extent.height}}; - projectionViews[i].subImage.imageArrayIndex = i; + const bool canSubmitLayer = this->frameState.shouldRender && swapchains.size() >= 2; + if (canSubmitLayer) { + for (uint32_t i = 0; i < 2; ++i) { + projectionViews[i] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}; + projectionViews[i].pose = this->views[i].pose; + projectionViews[i].fov = this->views[i].fov; + projectionViews[i].subImage.swapchain = this->swapchains[i].handle; + projectionViews[i].subImage.imageRect = {{0, 0}, {(int32_t)extent.width, (int32_t)extent.height}}; + projectionViews[i].subImage.imageArrayIndex = 0; + } + layer.viewCount = 2; + layer.views = projectionViews; } - layer.viewCount = 2; - layer.views = projectionViews; - static const XrCompositionLayerBaseHeader* layers[1]; uint32_t layerCount = 0; - if (this->frameState.shouldRender) { + if (canSubmitLayer) { layers[layerCount++] = (XrCompositionLayerBaseHeader*)&layer; } ei.layerCount = layerCount; ei.layers = layers; - xrEndFrame(this->session, &ei); + XrResult r = xrEndFrame(this->session, &ei); + static uint32_t endFrameCount = 0; + if (++endFrameCount <= 5 || endFrameCount % 100 == 0) { + std::cout << "[XR] xrEndFrame #" << endFrameCount + << " result=" << r + << " layerCount=" << ei.layerCount << std::endl; + } + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrEndFrame failed: " << r + << " (layerCount=" << ei.layerCount << ")" << std::endl; + } } void XrContext::locateViews(XrTime predictedTime) { @@ -516,7 +594,20 @@ void XrContext::locateViews(XrTime predictedTime) { XrViewState vs{XR_TYPE_VIEW_STATE}; uint32_t count = 0; - xrLocateViews(this->session, &li, &vs, (uint32_t)this->views.size(), &count, this->views.data()); + XrResult r = xrLocateViews(this->session, &li, &vs, (uint32_t)this->views.size(), &count, this->views.data()); + if (r != XR_SUCCESS) { + std::cerr << "[XR] xrLocateViews failed: " << r << std::endl; + } else { + const bool orientationValid = (vs.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0; + const bool positionValid = (vs.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0; + static bool firstLocate = true; + if (firstLocate || !orientationValid || !positionValid) { + std::cout << "[XR] locateViews: orientation=" << orientationValid + << " position=" << positionValid + << " viewCount=" << count << std::endl; + firstLocate = false; + } + } } vk::Viewport XrContext::getViewport(uint32_t eye) const { diff --git a/attachments/openxr_engine/xr_context.h b/attachments/openxr_engine/xr_context.h index bc09e615a..5ee2273c5 100644 --- a/attachments/openxr_engine/xr_context.h +++ b/attachments/openxr_engine/xr_context.h @@ -40,6 +40,7 @@ class XrContext { XrInstance getXrInstance() const { return instance; } XrSystemId getSystemId() const { return systemId; } bool createSession(vk::PhysicalDevice physicalDevice, vk::Device device, uint32_t queueFamilyIndex, uint32_t queueIndex); + void updateReferenceSpacePose(const XrPosef& pose); void cleanup(); #if defined(PLATFORM_ANDROID) @@ -54,7 +55,7 @@ class XrContext { // Swapchain Management (Chapter 3 & 8) vk::Extent2D getRecommendedExtent() const; void createSwapchains(vk::Device device, vk::Format format, vk::Extent2D extent); - std::vector enumerateSwapchainImages(); // Returns images with 2 layers for multiview + std::vector enumerateSwapchainImages(uint32_t swapchainIdx = 0); vk::Extent2D getSwapchainExtent() const { return extent; } vk::Format getSwapchainFormat() const { return format; } @@ -64,11 +65,22 @@ class XrContext { // Session lifecycle void pollEvents(); + // True when the session is running and frames should be rendered. bool isSessionRunning() const { return sessionState == XR_SESSION_STATE_SYNCHRONIZED || sessionState == XR_SESSION_STATE_VISIBLE || sessionState == XR_SESSION_STATE_FOCUSED; } + // True when xrWaitFrame/xrBeginFrame/xrEndFrame should be called. + // Includes READY state because some runtimes (Monado) require xrWaitFrame + // to be called to advance from READY to SYNCHRONIZED. + bool isSessionFraming() const { + return sessionBegun && + (sessionState == XR_SESSION_STATE_READY || + sessionState == XR_SESSION_STATE_SYNCHRONIZED || + sessionState == XR_SESSION_STATE_VISIBLE || + sessionState == XR_SESSION_STATE_FOCUSED); + } XrSessionState getSessionState() const { return sessionState; } // Frame Lifecycle (Chapter 5) @@ -118,6 +130,7 @@ class XrContext { XrSystemId systemId; XrSession session; XrSpace appSpace; + bool sessionBegun = false; XrReferenceSpaceType referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; uint8_t requiredLuid[VK_LUID_SIZE] = {0}; From 36cbfa3a5e38e231a27190691ba291baf3153fd4 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 30 Jun 2026 19:08:18 -0700 Subject: [PATCH 21/23] Fix OpenXR Vulkan Spatial annotations link in REUSE.toml --- REUSE.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/REUSE.toml b/REUSE.toml index 0731bd029..b65163820 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -76,6 +76,7 @@ path = [ "en/Advanced_glTF/**", "en/AI_Assisted_Vulkan/**", "en/Advanced_Vulkan_Compute/**", + "en/OpenXR_Vulkan_Spatial_Computing/**", "scripts/**", ] precedence = "closest" From a1264c4527383d8f0ac8242894b44fd8d65862fb Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 30 Jun 2026 19:22:50 -0700 Subject: [PATCH 22/23] Fix CI. --- REUSE.toml | 1 + attachments/openxr_engine/renderer.h | 11 +++++++---- attachments/openxr_engine/renderer_resources.cpp | 10 ++++++---- attachments/openxr_engine/renderer_utils.cpp | 5 +++++ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index b65163820..20888d621 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -71,6 +71,7 @@ path = [ "attachments/ml_inference/**", "attachments/compute/**", "attachments/advanced_gltf/**", + "attachments/openxr_engine/**", "en/Synchronization/**", "en/ML_Inference/**", "en/Advanced_glTF/**", diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index b9d781e8e..d0698e53d 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -449,14 +449,14 @@ class Renderer { * @param texturePath The path to the texture file. * @return True if the texture was loaded successfully, false otherwise. */ - bool LoadTexture(const std::string& texturePath); + bool LoadTexture(const std::string& texturePath, bool cachePixels = false); // Asynchronous texture loading APIs (thread-pool backed). // The 'critical' flag is used to front-load important textures (e.g., // baseColor/albedo) so the scene looks mostly correct before the loading // screen disappears. Non-critical textures (normals, MR, AO, emissive) // can stream in after geometry is visible. - std::future LoadTextureAsync(const std::string& texturePath, bool critical = false); + std::future LoadTextureAsync(const std::string& texturePath, bool critical = false, bool cachePixels = false); /** * @brief Load a texture from raw image data in memory. @@ -471,7 +471,8 @@ class Renderer { const unsigned char* imageData, int width, int height, - int channels); + int channels, + bool cachePixels = false); // Asynchronous upload from memory (RGBA/RGB/other). Safe for concurrent calls. std::future LoadTextureFromMemoryAsync(const std::string& textureId, @@ -479,7 +480,8 @@ class Renderer { int width, int height, int channels, - bool critical = false); + bool critical = false, + bool cachePixels = false); // Progress query for UI uint32_t GetTextureTasksScheduled() const { @@ -1939,6 +1941,7 @@ class Renderer { // Upload perf getters public: + bool fileExists(const std::string& filename); uint64_t GetBytesUploadedTotal() const { return bytesUploadedTotal.load(std::memory_order_relaxed); } diff --git a/attachments/openxr_engine/renderer_resources.cpp b/attachments/openxr_engine/renderer_resources.cpp index 1248b520f..040855550 100644 --- a/attachments/openxr_engine/renderer_resources.cpp +++ b/attachments/openxr_engine/renderer_resources.cpp @@ -683,7 +683,7 @@ bool Renderer::createTextureSampler(TextureResources& resources) { } // Load texture from file (public wrapper for createTextureImage) -bool Renderer::LoadTexture(const std::string& texturePath) { +bool Renderer::LoadTexture(const std::string& texturePath, bool /*cachePixels*/) { ensureThreadLocalVulkanInit(); if (texturePath.empty()) { std::cerr << "LoadTexture: Empty texture path provided" << std::endl; @@ -760,7 +760,8 @@ bool Renderer::LoadTextureFromMemory(const std::string& textureId, const unsigned char* imageData, int width, int height, - int channels) { + int channels, + bool) { ensureThreadLocalVulkanInit(); const std::string resolvedId = ResolveTextureId(textureId); std::cout << "[LoadTextureFromMemory] start id=" << textureId << " -> resolved=" << resolvedId << " size=" << width << "x" << height << " ch=" << channels << std::endl; @@ -3000,7 +3001,7 @@ bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector Renderer::LoadTextureAsync(const std::string& texturePath, bool critical) { +std::future Renderer::LoadTextureAsync(const std::string& texturePath, bool critical, bool) { if (texturePath.empty()) { return std::async(std::launch::deferred, [] { return false; }); } @@ -3038,7 +3039,8 @@ std::future Renderer::LoadTextureFromMemoryAsync(const std::string& textur int width, int height, int channels, - bool critical) { + bool critical, + bool) { if (!imageData || textureId.empty() || width <= 0 || height <= 0 || channels <= 0) { return std::async(std::launch::deferred, [] { return false; }); } diff --git a/attachments/openxr_engine/renderer_utils.cpp b/attachments/openxr_engine/renderer_utils.cpp index d16f16f74..a5104a046 100644 --- a/attachments/openxr_engine/renderer_utils.cpp +++ b/attachments/openxr_engine/renderer_utils.cpp @@ -16,6 +16,7 @@ */ #include "renderer.h" #include +#include #include #include #include @@ -112,6 +113,10 @@ std::vector Renderer::readFile(const std::string& filename) { } } +bool Renderer::fileExists(const std::string& filename) { + return std::filesystem::exists(filename); +} + // Create shader module vk::raii::ShaderModule Renderer::createShaderModule(const std::vector& code) { try { From bb7649632fc936e3474b1d8f60c6e414c2b4113e Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 30 Jun 2026 20:16:21 -0700 Subject: [PATCH 23/23] Fix CI. --- attachments/openxr_engine/renderer.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/attachments/openxr_engine/renderer.h b/attachments/openxr_engine/renderer.h index d0698e53d..7231a3f92 100644 --- a/attachments/openxr_engine/renderer.h +++ b/attachments/openxr_engine/renderer.h @@ -1920,8 +1920,6 @@ class Renderer { vk::Format findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features); bool hasStencilComponent(vk::Format format); - std::vector readFile(const std::string& filename); - // Background uploader helpers void StartUploadsWorker(size_t workerCount = 0); void StopUploadsWorker(); @@ -1941,6 +1939,7 @@ class Renderer { // Upload perf getters public: + std::vector readFile(const std::string& filename); bool fileExists(const std::string& filename); uint64_t GetBytesUploadedTotal() const { return bytesUploadedTotal.load(std::memory_order_relaxed);