From c4d529fa7581cf3c2686c88956de48f26d60d348 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 16 Dec 2025 02:53:07 -0800 Subject: [PATCH 01/24] Apply consistent formatting, improve code readability, and add/update copyright headers across multiple files. This is the full simple game engine with all features implemented including: streaming textures in separate threads with transfer queue ray query rendeirng pipeline forward+ rendering pipeline reflections thick and thin glass translucency better lighting fixes so light doesn't appear over bright animation from GLTF robustness2 synchronization2 Dynamic rendering local read shader tile image culling Incorporated PRs for: Android support Windows support memory leak fixes --- attachments/simple_engine/CMakeLists.txt | 128 +- .../simple_engine/animation_component.cpp | 298 + .../simple_engine/animation_component.h | 234 + attachments/simple_engine/audio_system.cpp | 3127 +-- attachments/simple_engine/audio_system.h | 792 +- .../simple_engine/camera_component.cpp | 111 +- attachments/simple_engine/camera_component.h | 451 +- attachments/simple_engine/component.cpp | 16 + attachments/simple_engine/component.h | 149 +- attachments/simple_engine/crash_reporter.h | 561 +- attachments/simple_engine/debug_system.h | 482 +- .../simple_engine/descriptor_manager.cpp | 406 +- .../simple_engine/descriptor_manager.h | 216 +- attachments/simple_engine/engine.cpp | 1776 +- attachments/simple_engine/engine.h | 706 +- attachments/simple_engine/entity.cpp | 62 +- attachments/simple_engine/entity.h | 294 +- attachments/simple_engine/imgui/imconfig.h | 19 +- attachments/simple_engine/imgui/imgui.cpp | 21666 ++++++++-------- attachments/simple_engine/imgui/imgui.h | 3319 ++- .../simple_engine/imgui/imgui_draw.cpp | 6852 +++-- .../simple_engine/imgui/imgui_internal.h | 2184 +- .../simple_engine/imgui/stb_rect_pack.h | 869 +- .../simple_engine/imgui/stb_textedit.h | 1654 +- .../simple_engine/imgui/stb_truetype.h | 5189 ++-- attachments/simple_engine/imgui_system.cpp | 2110 +- attachments/simple_engine/imgui_system.h | 466 +- attachments/simple_engine/main.cpp | 170 +- attachments/simple_engine/memory_pool.cpp | 1061 +- attachments/simple_engine/memory_pool.h | 386 +- attachments/simple_engine/mesh_component.cpp | 190 +- attachments/simple_engine/mesh_component.h | 940 +- attachments/simple_engine/mikktspace.h | 219 +- attachments/simple_engine/model_loader.cpp | 3976 +-- attachments/simple_engine/model_loader.h | 669 +- attachments/simple_engine/physics_system.cpp | 2792 +- attachments/simple_engine/physics_system.h | 754 +- attachments/simple_engine/pipeline.cpp | 1385 +- attachments/simple_engine/pipeline.h | 381 +- attachments/simple_engine/platform.cpp | 944 +- attachments/simple_engine/platform.h | 903 +- .../simple_engine/renderdoc_debug_system.cpp | 225 +- .../simple_engine/renderdoc_debug_system.h | 89 +- attachments/simple_engine/renderer.h | 2485 +- .../simple_engine/renderer_compute.cpp | 824 +- attachments/simple_engine/renderer_core.cpp | 1702 +- .../simple_engine/renderer_pipelines.cpp | 2066 +- .../simple_engine/renderer_ray_query.cpp | 1455 ++ .../simple_engine/renderer_rendering.cpp | 3979 ++- .../simple_engine/renderer_resources.cpp | 5984 +++-- attachments/simple_engine/renderer_utils.cpp | 527 +- .../simple_engine/resource_manager.cpp | 49 +- attachments/simple_engine/resource_manager.h | 503 +- attachments/simple_engine/scene_loading.cpp | 914 +- attachments/simple_engine/scene_loading.h | 28 +- .../simple_engine/shaders/common_types.slang | 146 + .../simple_engine/shaders/composite.slang | 80 + .../shaders/forward_plus_cull.slang | 145 + attachments/simple_engine/shaders/hrtf.slang | 16 + attachments/simple_engine/shaders/imgui.slang | 16 + .../simple_engine/shaders/lighting.slang | 16 + .../shaders/lighting_utils.slang | 101 + attachments/simple_engine/shaders/pbr.slang | 424 +- .../simple_engine/shaders/pbr_utils.slang | 55 + .../simple_engine/shaders/physics.slang | 16 + .../simple_engine/shaders/ray_query.slang | 849 + .../simple_engine/shaders/texturedMesh.slang | 16 + .../shaders/tonemapping_utils.slang | 34 + attachments/simple_engine/swap_chain.h | 210 +- attachments/simple_engine/thread_pool.h | 169 +- .../simple_engine/transform_component.cpp | 51 +- .../simple_engine/transform_component.h | 258 +- attachments/simple_engine/vulkan_device.cpp | 549 +- attachments/simple_engine/vulkan_device.h | 294 +- attachments/simple_engine/vulkan_dispatch.cpp | 21 +- 75 files changed, 54943 insertions(+), 38260 deletions(-) create mode 100644 attachments/simple_engine/animation_component.cpp create mode 100644 attachments/simple_engine/animation_component.h create mode 100644 attachments/simple_engine/renderer_ray_query.cpp create mode 100644 attachments/simple_engine/shaders/common_types.slang create mode 100644 attachments/simple_engine/shaders/composite.slang create mode 100644 attachments/simple_engine/shaders/forward_plus_cull.slang create mode 100644 attachments/simple_engine/shaders/lighting_utils.slang create mode 100644 attachments/simple_engine/shaders/pbr_utils.slang create mode 100644 attachments/simple_engine/shaders/ray_query.slang create mode 100644 attachments/simple_engine/shaders/tonemapping_utils.slang diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt index 4489b87e..a46736c8 100644 --- a/attachments/simple_engine/CMakeLists.txt +++ b/attachments/simple_engine/CMakeLists.txt @@ -1,12 +1,17 @@ cmake_minimum_required(VERSION 3.29) -# Enable C++ module dependency scanning -set(CMAKE_CXX_SCAN_FOR_MODULES ON) - project(SimpleEngine 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 SimpleEngine" OFF) + +# Enable C++ module dependency scanning only when modules are enabled +if(ENABLE_CPP20_MODULE) + set(CMAKE_CXX_SCAN_FOR_MODULES ON) +endif() + # Add CMake module path for custom find modules -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../CMake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake") # Find required packages find_package (glfw3 REQUIRED) @@ -16,32 +21,52 @@ find_package (tinygltf REQUIRED) find_package (KTX REQUIRED) find_package (OpenAL REQUIRED) -# set up Vulkan C++ module -add_library(VulkanCppModule) -add_library(Vulkan::cppm ALIAS VulkanCppModule) +if(ENABLE_CPP20_MODULE) + # Set up Vulkan C++ module for this standalone project + 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 - PRIVATE - "${Vulkan_INCLUDE_DIR}" -) -target_link_libraries(VulkanCppModule - PUBLIC - Vulkan::Vulkan -) + 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}" + ) + target_link_libraries(VulkanCppModule + PUBLIC + Vulkan::Vulkan + ) -set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) -target_sources(VulkanCppModule - PUBLIC - FILE_SET cxx_modules TYPE CXX_MODULES - BASE_DIRS - "${Vulkan_INCLUDE_DIR}" - FILES - "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" -) + target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_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 + ) +endif() @@ -55,8 +80,9 @@ else() endif() # Shader compilation -# Find Slang shaders +# Find Slang shaders (exclude utility modules that are imported, not compiled standalone) file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.slang) +list(FILTER SLANG_SHADER_SOURCES EXCLUDE REGEX ".*/(common_types|pbr_utils|lighting_utils|tonemapping_utils)\\.slang$") # Find slangc executable (optional) find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin) @@ -97,6 +123,7 @@ set(SOURCES renderer_compute.cpp renderer_utils.cpp renderer_resources.cpp + renderer_ray_query.cpp memory_pool.cpp resource_manager.cpp entity.cpp @@ -104,6 +131,7 @@ set(SOURCES transform_component.cpp mesh_component.cpp camera_component.cpp + animation_component.cpp model_loader.cpp audio_system.cpp physics_system.cpp @@ -122,9 +150,24 @@ add_executable(SimpleEngine ${SOURCES}) add_dependencies(SimpleEngine shaders) set_target_properties (SimpleEngine PROPERTIES CXX_STANDARD 20) +# Enable required defines for GLM experimental extensions and MSVC math constants +target_compile_definitions(SimpleEngine PRIVATE + GLM_ENABLE_EXPERIMENTAL + _USE_MATH_DEFINES + VULKAN_HPP_NO_STRUCT_CONSTRUCTORS + VULKAN_HPP_DISPATCH_LOADER_DYNAMIC +) + # Link libraries +# Prefer the Vulkan C++ module target when available (configured at the parent level), +# but fall back to the standard Vulkan library otherwise. +if(TARGET Vulkan::cppm) + target_link_libraries(SimpleEngine PRIVATE Vulkan::cppm) +else() + target_link_libraries(SimpleEngine PRIVATE Vulkan::Vulkan) +endif() + target_link_libraries(SimpleEngine PRIVATE - Vulkan::cppm glm::glm tinygltf::tinygltf KTX::ktx @@ -135,6 +178,33 @@ if(NOT ANDROID) target_link_libraries(SimpleEngine PRIVATE glfw) endif() +# Windows/MSVC portability and build settings +if(MSVC) + # Avoid Windows.h macro pollution and CRT warnings; improve conformance and build perf + target_compile_definitions(SimpleEngine PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) + target_compile_options(SimpleEngine PRIVATE + /permissive- + /Zc:__cplusplus + /EHsc + /W3 + /MP + /bigobj + ) + # Crash reporter uses Dbghelp; pragma should suffice, but make it explicit for clarity + target_link_libraries(SimpleEngine PRIVATE Dbghelp) +elseif(WIN32) + # Non-MSVC Windows toolchains (e.g., MinGW) + target_compile_definitions(SimpleEngine PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) +endif() + # Copy model and texture files if they exist if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) add_custom_command(TARGET SimpleEngine POST_BUILD diff --git a/attachments/simple_engine/animation_component.cpp b/attachments/simple_engine/animation_component.cpp new file mode 100644 index 00000000..570058f4 --- /dev/null +++ b/attachments/simple_engine/animation_component.cpp @@ -0,0 +1,298 @@ +/* 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 "animation_component.h" +#include "entity.h" +#include "transform_component.h" + +#include +#include + +void AnimationComponent::Update(std::chrono::milliseconds deltaTime) +{ + if (!playing || currentAnimationIndex < 0 || + currentAnimationIndex >= static_cast(animations.size())) + { + return; + } + + const Animation &anim = animations[currentAnimationIndex]; + float duration = anim.GetDuration(); + + if (duration <= 0.0f) + { + return; + } + + // Advance time + float dt = static_cast(deltaTime.count()) * 0.001f * playbackSpeed; + currentTime += dt; + + // Handle looping or stopping at the end + if (currentTime >= duration) + { + if (looping) + { + currentTime = std::fmod(currentTime, duration); + } + else + { + currentTime = duration; + playing = false; + } + } + + // Capture base transforms on first update if not already captured + if (basePositions.empty()) + { + for (const auto &[nodeIndex, entity] : nodeToEntity) + { + if (entity) + { + auto *transform = entity->GetComponent(); + if (transform) + { + basePositions[nodeIndex] = transform->GetPosition(); + // Convert Euler angles to quaternion for proper rotation composition + glm::vec3 eulerAngles = transform->GetRotation(); + baseRotations[nodeIndex] = glm::quat(eulerAngles); + baseScales[nodeIndex] = transform->GetScale(); + } + } + } + } + + // Apply animation to all channels + for (const auto &channel : anim.channels) + { + if (channel.samplerIndex < 0 || + channel.samplerIndex >= static_cast(anim.samplers.size())) + { + continue; + } + + const AnimationSampler &sampler = anim.samplers[channel.samplerIndex]; + + // Find the target entity for this node + auto entityIt = nodeToEntity.find(channel.targetNode); + if (entityIt == nodeToEntity.end() || !entityIt->second) + { + continue; + } + + Entity *targetEntity = entityIt->second; + auto *transform = targetEntity->GetComponent(); + if (!transform) + { + continue; + } + + // Get base transform for this node (defaults to identity if not found) + glm::vec3 basePos = basePositions.count(channel.targetNode) ? + basePositions[channel.targetNode] : + glm::vec3(0.0f); + glm::quat baseRot = baseRotations.count(channel.targetNode) ? + baseRotations[channel.targetNode] : + glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 baseScale = baseScales.count(channel.targetNode) ? + baseScales[channel.targetNode] : + glm::vec3(1.0f); + + // Sample and apply the appropriate transform property + // Animation transforms are applied RELATIVE to the base transform + switch (channel.path) + { + case AnimationPath::Translation: + { + glm::vec3 animTranslation = SampleVec3(sampler, currentTime); + // Apply animation translation relative to base position + transform->SetPosition(basePos + animTranslation); + break; + } + case AnimationPath::Rotation: + { + glm::quat animRotation = SampleQuat(sampler, currentTime); + // Compose rotations using quaternion multiplication (order matters!) + // Final rotation = base rotation * animation delta rotation + glm::quat finalRotation = baseRot * animRotation; + // Convert to Euler angles for the transform component + transform->SetRotation(glm::eulerAngles(finalRotation)); + break; + } + case AnimationPath::Scale: + { + glm::vec3 animScale = SampleVec3(sampler, currentTime); + // Multiply scales (animation scale is a factor, not an offset) + transform->SetScale(baseScale * animScale); + break; + } + case AnimationPath::Weights: + // Morph target weights not yet implemented + break; + } + } +} + +void AnimationComponent::FindKeyframes(const std::vector ×, float time, + size_t &outIndex0, size_t &outIndex1, float &outT) const +{ + if (times.empty()) + { + outIndex0 = 0; + outIndex1 = 0; + outT = 0.0f; + return; + } + + if (times.size() == 1 || time <= times.front()) + { + outIndex0 = 0; + outIndex1 = 0; + outT = 0.0f; + return; + } + + if (time >= times.back()) + { + outIndex0 = times.size() - 1; + outIndex1 = times.size() - 1; + outT = 0.0f; + return; + } + + // Binary search for the keyframe + auto it = std::lower_bound(times.begin(), times.end(), time); + if (it == times.begin()) + { + outIndex0 = 0; + outIndex1 = 0; + outT = 0.0f; + return; + } + + outIndex1 = static_cast(std::distance(times.begin(), it)); + outIndex0 = outIndex1 - 1; + + float t0 = times[outIndex0]; + float t1 = times[outIndex1]; + float dt = t1 - t0; + + if (dt > 0.0f) + { + outT = (time - t0) / dt; + } + else + { + outT = 0.0f; + } +} + +glm::vec3 AnimationComponent::SampleVec3(const AnimationSampler &sampler, float time) const +{ + if (sampler.inputTimes.empty() || sampler.outputValues.size() < 3) + { + return glm::vec3(0.0f); + } + + size_t index0, index1; + float t; + FindKeyframes(sampler.inputTimes, time, index0, index1, t); + + // Get values at keyframes (3 floats per vec3) + size_t offset0 = index0 * 3; + size_t offset1 = index1 * 3; + + if (offset0 + 2 >= sampler.outputValues.size()) + { + offset0 = sampler.outputValues.size() - 3; + } + if (offset1 + 2 >= sampler.outputValues.size()) + { + offset1 = sampler.outputValues.size() - 3; + } + + glm::vec3 v0(sampler.outputValues[offset0], + sampler.outputValues[offset0 + 1], + sampler.outputValues[offset0 + 2]); + glm::vec3 v1(sampler.outputValues[offset1], + sampler.outputValues[offset1 + 1], + sampler.outputValues[offset1 + 2]); + + // Interpolate based on interpolation type + switch (sampler.interpolation) + { + case AnimationInterpolation::Step: + return v0; + case AnimationInterpolation::Linear: + return glm::mix(v0, v1, t); + case AnimationInterpolation::CubicSpline: + // For cubic spline, the output has in-tangent, value, out-tangent + // Simplified: just use linear interpolation for now + // Full cubic spline would require reading tangents from output data + return glm::mix(v0, v1, t); + default: + return glm::mix(v0, v1, t); + } +} + +glm::quat AnimationComponent::SampleQuat(const AnimationSampler &sampler, float time) const +{ + if (sampler.inputTimes.empty() || sampler.outputValues.size() < 4) + { + return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + } + + size_t index0, index1; + float t; + FindKeyframes(sampler.inputTimes, time, index0, index1, t); + + // Get values at keyframes (4 floats per quaternion: x, y, z, w) + size_t offset0 = index0 * 4; + size_t offset1 = index1 * 4; + + if (offset0 + 3 >= sampler.outputValues.size()) + { + offset0 = sampler.outputValues.size() - 4; + } + if (offset1 + 3 >= sampler.outputValues.size()) + { + offset1 = sampler.outputValues.size() - 4; + } + + // glTF quaternions are stored as (x, y, z, w) + glm::quat q0(sampler.outputValues[offset0 + 3], // w + sampler.outputValues[offset0], // x + sampler.outputValues[offset0 + 1], // y + sampler.outputValues[offset0 + 2]); // z + glm::quat q1(sampler.outputValues[offset1 + 3], // w + sampler.outputValues[offset1], // x + sampler.outputValues[offset1 + 1], // y + sampler.outputValues[offset1 + 2]); // z + + // Interpolate based on interpolation type + switch (sampler.interpolation) + { + case AnimationInterpolation::Step: + return q0; + case AnimationInterpolation::Linear: + return glm::slerp(q0, q1, t); + case AnimationInterpolation::CubicSpline: + // Simplified: use slerp for now + return glm::slerp(q0, q1, t); + default: + return glm::slerp(q0, q1, t); + } +} diff --git a/attachments/simple_engine/animation_component.h b/attachments/simple_engine/animation_component.h new file mode 100644 index 00000000..64793f94 --- /dev/null +++ b/attachments/simple_engine/animation_component.h @@ -0,0 +1,234 @@ +/* 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 "model_loader.h" + +class Entity; +class TransformComponent; + +/** + * @brief Component that handles skeletal/transform animation playback. + * + * This component stores animation clips and plays them back by interpolating + * keyframes and applying transforms to target nodes (entities). + */ +class AnimationComponent final : public Component +{ + public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit AnimationComponent(const std::string &componentName = "AnimationComponent") : + Component(componentName) + {} + + /** + * @brief Set the animations for this component. + * @param anims Vector of Animation clips to use. + */ + void SetAnimations(const std::vector &anims) + { + animations = anims; + if (!animations.empty()) + { + currentAnimationIndex = 0; + } + } + + /** + * @brief Get the animations stored in this component. + * @return Reference to the animations vector. + */ + [[nodiscard]] const std::vector &GetAnimations() const + { + return animations; + } + + /** + * @brief Set the mapping from glTF node indices to entity pointers. + * This allows the animation system to apply transforms to the correct entities. + * @param mapping Map from node index to Entity pointer. + */ + void SetNodeToEntityMap(const std::unordered_map &mapping) + { + nodeToEntity = mapping; + } + + /** + * @brief Play an animation by index. + * @param index The index of the animation to play. + * @param loop Whether to loop the animation (default: true). + */ + void Play(size_t index, bool loop = true) + { + if (index < animations.size()) + { + currentAnimationIndex = static_cast(index); + currentTime = 0.0f; + playing = true; + looping = loop; + } + } + + /** + * @brief Play an animation by name. + * @param name The name of the animation to play. + * @param loop Whether to loop the animation (default: true). + */ + void PlayByName(const std::string &name, bool loop = true) + { + for (size_t i = 0; i < animations.size(); ++i) + { + if (animations[i].name == name) + { + Play(i, loop); + return; + } + } + } + + /** + * @brief Stop the current animation. + */ + void Stop() + { + playing = false; + } + + /** + * @brief Pause the current animation. + */ + void Pause() + { + playing = false; + } + + /** + * @brief Resume a paused animation. + */ + void Resume() + { + playing = true; + } + + /** + * @brief Check if an animation is currently playing. + * @return True if playing, false otherwise. + */ + [[nodiscard]] bool IsPlaying() const + { + return playing; + } + + /** + * @brief Set the playback speed multiplier. + * @param speed The speed multiplier (1.0 = normal speed). + */ + void SetSpeed(float speed) + { + playbackSpeed = speed; + } + + /** + * @brief Get the current playback speed. + * @return The playback speed multiplier. + */ + [[nodiscard]] float GetSpeed() const + { + return playbackSpeed; + } + + /** + * @brief Get the current animation time. + * @return The current time in seconds. + */ + [[nodiscard]] float GetCurrentTime() const + { + return currentTime; + } + + /** + * @brief Get the duration of the current animation. + * @return The duration in seconds, or 0 if no animation is selected. + */ + [[nodiscard]] float GetCurrentDuration() const + { + if (currentAnimationIndex >= 0 && currentAnimationIndex < static_cast(animations.size())) + { + return animations[currentAnimationIndex].GetDuration(); + } + return 0.0f; + } + + /** + * @brief Update the animation, advancing time and applying transforms. + * @param deltaTime The time elapsed since the last update. + */ + void Update(std::chrono::milliseconds deltaTime) override; + + private: + std::vector animations; + std::unordered_map nodeToEntity; // Maps glTF node index to Entity + + // Store base transforms for each animated node (captured when animation starts) + // Animation transforms are applied relative to these base transforms + std::unordered_map basePositions; + std::unordered_map baseRotations; // Quaternions for proper rotation composition + std::unordered_map baseScales; + + int currentAnimationIndex = -1; + float currentTime = 0.0f; + float playbackSpeed = 1.0f; + bool playing = false; + bool looping = true; + + /** + * @brief Sample a vec3 value from a sampler at a given time. + * @param sampler The animation sampler. + * @param time The time to sample at. + * @return The interpolated vec3 value. + */ + [[nodiscard]] glm::vec3 SampleVec3(const AnimationSampler &sampler, float time) const; + + /** + * @brief Sample a quaternion value from a sampler at a given time. + * @param sampler The animation sampler. + * @param time The time to sample at. + * @return The interpolated quaternion value. + */ + [[nodiscard]] glm::quat SampleQuat(const AnimationSampler &sampler, float time) const; + + /** + * @brief Find the keyframe indices for interpolation. + * @param times The input time array. + * @param time The current time. + * @param outIndex0 Output: the lower keyframe index. + * @param outIndex1 Output: the upper keyframe index. + * @param outT Output: the interpolation factor (0-1). + */ + void FindKeyframes(const std::vector ×, float time, + size_t &outIndex0, size_t &outIndex1, float &outT) const; +}; diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp index 9cbbd56c..a4bb8cb7 100644 --- a/attachments/simple_engine/audio_system.cpp +++ b/attachments/simple_engine/audio_system.cpp @@ -1,1556 +1,1755 @@ +/* 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 "audio_system.h" +#include #include +#include #include #include #include #include #include -#include -#include -#include #include -#include +#include +#include #include -#include +#include // OpenAL headers #ifdef __APPLE__ -#include -#include +# include +# include #else -#include -#include +# include +# include #endif -#include "renderer.h" #include "engine.h" +#include "renderer.h" // OpenAL error checking utility -static void CheckOpenALError(const std::string& operation) { - ALenum error = alGetError(); - if (error != AL_NO_ERROR) { - std::cerr << "OpenAL Error in " << operation << ": "; - switch (error) { - case AL_INVALID_NAME: - std::cerr << "AL_INVALID_NAME"; - break; - case AL_INVALID_ENUM: - std::cerr << "AL_INVALID_ENUM"; - break; - case AL_INVALID_VALUE: - std::cerr << "AL_INVALID_VALUE"; - break; - case AL_INVALID_OPERATION: - std::cerr << "AL_INVALID_OPERATION"; - break; - case AL_OUT_OF_MEMORY: - std::cerr << "AL_OUT_OF_MEMORY"; - break; - default: - std::cerr << "Unknown error " << error; - break; - } - std::cerr << std::endl; - } +static void CheckOpenALError(const std::string &operation) +{ + ALenum error = alGetError(); + if (error != AL_NO_ERROR) + { + std::cerr << "OpenAL Error in " << operation << ": "; + switch (error) + { + case AL_INVALID_NAME: + std::cerr << "AL_INVALID_NAME"; + break; + case AL_INVALID_ENUM: + std::cerr << "AL_INVALID_ENUM"; + break; + case AL_INVALID_VALUE: + std::cerr << "AL_INVALID_VALUE"; + break; + case AL_INVALID_OPERATION: + std::cerr << "AL_INVALID_OPERATION"; + break; + case AL_OUT_OF_MEMORY: + std::cerr << "AL_OUT_OF_MEMORY"; + break; + default: + std::cerr << "Unknown error " << error; + break; + } + std::cerr << std::endl; + } } // Concrete implementation of AudioSource -class ConcreteAudioSource : public AudioSource { -public: - explicit ConcreteAudioSource(std::string name) : name(std::move(name)) {} - ~ConcreteAudioSource() override = default; - - void Play() override { - playing = true; - playbackPosition = 0; - delayTimer = std::chrono::milliseconds(0); - inDelayPhase = false; - sampleAccumulator = 0.0; - } - - void Pause() override { - playing = false; - } - - void Stop() override { - playing = false; - playbackPosition = 0; - delayTimer = std::chrono::milliseconds(0); - inDelayPhase = false; - sampleAccumulator = 0.0; - } - - void SetVolume(float volume) override { - this->volume = volume; - } - - void SetLoop(bool loop) override { - this->loop = loop; - } - - void SetPosition(float x, float y, float z) override { - position[0] = x; - position[1] = y; - position[2] = z; - } - - void SetVelocity(float x, float y, float z) override { - velocity[0] = x; - velocity[1] = y; - velocity[2] = z; - } - - [[nodiscard]] bool IsPlaying() const override { - return playing; - } - - // Additional methods for delay functionality - void SetAudioLength(uint32_t lengthInSamples) { - audioLengthSamples = lengthInSamples; - } - - void UpdatePlayback(std::chrono::milliseconds deltaTime, uint32_t samplesProcessed) { - if (!playing) return; - - if (inDelayPhase) { - // We're in the delay phase between playthroughs - delayTimer += deltaTime; - if (delayTimer >= delayDuration) { - // Delay finished, restart playback - inDelayPhase = false; - playbackPosition = 0; - delayTimer = std::chrono::milliseconds(0); - } - } else { - // Normal playback, update position - playbackPosition += samplesProcessed; - - // Check if we've reached the end of the audio - if (audioLengthSamples > 0 && playbackPosition >= audioLengthSamples) { - if (loop) { - // Start the delay phase before looping - inDelayPhase = true; - delayTimer = std::chrono::milliseconds(0); - } else { - // Stop playing if not looping - playing = false; - playbackPosition = 0; - } - } - } - } - - [[nodiscard]] bool ShouldProcessAudio() const { - return playing && !inDelayPhase; - } - - [[nodiscard]] uint32_t GetPlaybackPosition() const { - return playbackPosition; - } - - [[nodiscard]] const std::string& GetName() const { - return name; - } - - [[nodiscard]] const float* GetPosition() const { - return position; - } - - [[nodiscard]] double GetSampleAccumulator() const { - return sampleAccumulator; - } - - void SetSampleAccumulator(double value) { - sampleAccumulator = value; - } - -private: - std::string name; - bool playing = false; - bool loop = false; - float volume = 1.0f; - float position[3] = {0.0f, 0.0f, 0.0f}; - float velocity[3] = {0.0f, 0.0f, 0.0f}; - - // Delay and timing functionality - uint32_t playbackPosition = 0; // Current position in samples - uint32_t audioLengthSamples = 0; // Total length of audio in samples - std::chrono::milliseconds delayTimer = std::chrono::milliseconds(0); // Timer for delay between loops - bool inDelayPhase = false; // Whether we're currently in the delay phase - static constexpr std::chrono::milliseconds delayDuration = std::chrono::milliseconds(1500); // 1.5-second delay between loops - double sampleAccumulator = 0.0; // Per-source sample accumulator for proper timing +class ConcreteAudioSource : public AudioSource +{ + public: + explicit ConcreteAudioSource(std::string name) : + name(std::move(name)) + {} + ~ConcreteAudioSource() override = default; + + void Play() override + { + playing = true; + playbackPosition = 0; + delayTimer = std::chrono::milliseconds(0); + inDelayPhase = false; + sampleAccumulator = 0.0; + } + + void Pause() override + { + playing = false; + } + + void Stop() override + { + playing = false; + playbackPosition = 0; + delayTimer = std::chrono::milliseconds(0); + inDelayPhase = false; + sampleAccumulator = 0.0; + } + + void SetVolume(float volume) override + { + this->volume = volume; + } + + void SetLoop(bool loop) override + { + this->loop = loop; + } + + void SetPosition(float x, float y, float z) override + { + position[0] = x; + position[1] = y; + position[2] = z; + } + + void SetVelocity(float x, float y, float z) override + { + velocity[0] = x; + velocity[1] = y; + velocity[2] = z; + } + + [[nodiscard]] bool IsPlaying() const override + { + return playing; + } + + // Additional methods for delay functionality + void SetAudioLength(uint32_t lengthInSamples) + { + audioLengthSamples = lengthInSamples; + } + + void UpdatePlayback(std::chrono::milliseconds deltaTime, uint32_t samplesProcessed) + { + if (!playing) + return; + + if (inDelayPhase) + { + // We're in the delay phase between playthroughs + delayTimer += deltaTime; + if (delayTimer >= delayDuration) + { + // Delay finished, restart playback + inDelayPhase = false; + playbackPosition = 0; + delayTimer = std::chrono::milliseconds(0); + } + } + else + { + // Normal playback, update position + playbackPosition += samplesProcessed; + + // Check if we've reached the end of the audio + if (audioLengthSamples > 0 && playbackPosition >= audioLengthSamples) + { + if (loop) + { + // Start the delay phase before looping + inDelayPhase = true; + delayTimer = std::chrono::milliseconds(0); + } + else + { + // Stop playing if not looping + playing = false; + playbackPosition = 0; + } + } + } + } + + [[nodiscard]] bool ShouldProcessAudio() const + { + return playing && !inDelayPhase; + } + + [[nodiscard]] uint32_t GetPlaybackPosition() const + { + return playbackPosition; + } + + [[nodiscard]] const std::string &GetName() const + { + return name; + } + + [[nodiscard]] const float *GetPosition() const + { + return position; + } + + [[nodiscard]] double GetSampleAccumulator() const + { + return sampleAccumulator; + } + + void SetSampleAccumulator(double value) + { + sampleAccumulator = value; + } + + private: + std::string name; + bool playing = false; + bool loop = false; + float volume = 1.0f; + float position[3] = {0.0f, 0.0f, 0.0f}; + float velocity[3] = {0.0f, 0.0f, 0.0f}; + + // Delay and timing functionality + uint32_t playbackPosition = 0; // Current position in samples + uint32_t audioLengthSamples = 0; // Total length of audio in samples + std::chrono::milliseconds delayTimer = std::chrono::milliseconds(0); // Timer for delay between loops + bool inDelayPhase = false; // Whether we're currently in the delay phase + static constexpr std::chrono::milliseconds delayDuration = std::chrono::milliseconds(1500); // 1.5-second delay between loops + double sampleAccumulator = 0.0; // Per-source sample accumulator for proper timing }; // OpenAL audio output device implementation -class OpenALAudioOutputDevice : public AudioOutputDevice { -public: - OpenALAudioOutputDevice() = default; - ~OpenALAudioOutputDevice() override { - OpenALAudioOutputDevice::Stop(); - Cleanup(); - } - - bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) override { - this->sampleRate = sampleRate; - this->channels = channels; - this->bufferSize = bufferSize; - - // Initialize OpenAL - device = alcOpenDevice(nullptr); // Use default device - if (!device) { - std::cerr << "Failed to open OpenAL device" << std::endl; - return false; - } - - context = alcCreateContext(device, nullptr); - if (!context) { - std::cerr << "Failed to create OpenAL context" << std::endl; - alcCloseDevice(device); - device = nullptr; - return false; - } - - if (!alcMakeContextCurrent(context)) { - std::cerr << "Failed to make OpenAL context current" << std::endl; - alcDestroyContext(context); - alcCloseDevice(device); - context = nullptr; - device = nullptr; - return false; - } - - // Generate OpenAL source - alGenSources(1, &source); - CheckOpenALError("alGenSources"); - - // Generate OpenAL buffers for streaming - alGenBuffers(NUM_BUFFERS, buffers); - CheckOpenALError("alGenBuffers"); - - // Set source properties - alSourcef(source, AL_PITCH, 1.0f); - alSourcef(source, AL_GAIN, 1.0f); - alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); - alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); - alSourcei(source, AL_LOOPING, AL_FALSE); - CheckOpenALError("Source setup"); - - // Initialize audio buffer - audioBuffer.resize(bufferSize * channels); - - // Initialize buffer tracking - queuedBufferCount = 0; - while (!availableBuffers.empty()) { - availableBuffers.pop(); - } - - initialized = true; - return true; - } - - bool Start() override { - if (!initialized) { - std::cerr << "OpenAL audio output device not initialized" << std::endl; - return false; - } - - if (playing) { - return true; // Already playing - } - - playing = true; - - // Start an audio playback thread - audioThread = std::thread(&OpenALAudioOutputDevice::AudioThreadFunction, this); - - return true; - } - - bool Stop() override { - if (!playing) { - return true; // Already stopped - } - - playing = false; - - // Wait for the audio thread to finish - if (audioThread.joinable()) { - audioThread.join(); - } - - // Stop OpenAL source - if (initialized && source != 0) { - alSourceStop(source); - CheckOpenALError("alSourceStop"); - } - - return true; - } - - bool WriteAudio(const float* data, uint32_t sampleCount) override { - if (!initialized || !playing) { - return false; - } - - std::lock_guard lock(bufferMutex); - - // Add audio data to the queue - for (uint32_t i = 0; i < sampleCount * channels; i++) { - audioQueue.push(data[i]); - } - - return true; - } - - [[nodiscard]] bool IsPlaying() const override { - return playing; - } - - [[nodiscard]] uint32_t GetPosition() const override { - return playbackPosition; - } - -private: - static constexpr int NUM_BUFFERS = 8; - - uint32_t sampleRate = 44100; - uint32_t channels = 2; - uint32_t bufferSize = 1024; - bool initialized = false; - bool playing = false; - uint32_t playbackPosition = 0; - - // OpenAL objects - ALCdevice* device = nullptr; - ALCcontext* context = nullptr; - ALuint source = 0; - ALuint buffers[NUM_BUFFERS]{}; - int currentBuffer = 0; - - std::vector audioBuffer; - std::queue audioQueue; - std::mutex bufferMutex; - std::thread audioThread; - - // Buffer management for OpenAL streaming - std::queue availableBuffers; - int queuedBufferCount = 0; - - void Cleanup() { - if (initialized) { - // Clean up OpenAL resources - if (source != 0) { - alDeleteSources(1, &source); - source = 0; - } - - alDeleteBuffers(NUM_BUFFERS, buffers); - - if (context) { - alcMakeContextCurrent(nullptr); - alcDestroyContext(context); - context = nullptr; - } - - if (device) { - alcCloseDevice(device); - device = nullptr; - } - - // Reset buffer tracking - queuedBufferCount = 0; - while (!availableBuffers.empty()) { - availableBuffers.pop(); - } - - initialized = false; - } - } - - void AudioThreadFunction() { - // Calculate sleep time for audio buffer updates (in milliseconds) - const auto sleepTime = std::chrono::milliseconds( - static_cast((bufferSize * 1000) / sampleRate / 8) // Eighth buffer time for responsiveness - ); - - while (playing) { - ProcessAudioBuffer(); - std::this_thread::sleep_for(sleepTime); - } - } - - void ProcessAudioBuffer() { - std::lock_guard lock(bufferMutex); - - // Fill audio buffer from queue in whole stereo frames to preserve channel alignment - uint32_t samplesProcessed = 0; - const uint32_t framesAvailable = static_cast(audioQueue.size() / channels); - if (framesAvailable == 0) { - // Not enough data for a whole frame yet - return; - } - const uint32_t framesToSend = std::min(framesAvailable, bufferSize); - const uint32_t samplesToSend = framesToSend * channels; - for (uint32_t i = 0; i < samplesToSend; i++) { - audioBuffer[i] = audioQueue.front(); - audioQueue.pop(); - } - samplesProcessed = samplesToSend; - - if (samplesProcessed > 0) { - // Convert float samples to 16-bit PCM for OpenAL - std::vector pcmBuffer(samplesProcessed); - for (uint32_t i = 0; i < samplesProcessed; i++) { - // Clamp and convert to 16-bit PCM - float sample = std::clamp(audioBuffer[i], -1.0f, 1.0f); - pcmBuffer[i] = static_cast(sample * 32767.0f); - } - - // Check for processed buffers and unqueue them - ALint processed = 0; - alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); - CheckOpenALError("alGetSourcei AL_BUFFERS_PROCESSED"); - - // Unqueue processed buffers and add them to available buffers - while (processed > 0) { - ALuint buffer; - alSourceUnqueueBuffers(source, 1, &buffer); - CheckOpenALError("alSourceUnqueueBuffers"); - - // Add the unqueued buffer to available buffers - availableBuffers.push(buffer); - processed--; - } - - // Only proceed if we have an available buffer - ALuint buffer = 0; - if (!availableBuffers.empty()) { - buffer = availableBuffers.front(); - availableBuffers.pop(); - } else if (queuedBufferCount < NUM_BUFFERS) { - // Use a buffer that hasn't been queued yet - buffer = buffers[queuedBufferCount]; - } else { - // No available buffers, skip this frame - return; - } - - // Validate buffer parameters - if (pcmBuffer.empty()) { - // Re-add buffer to available list if we can't use it - if (queuedBufferCount >= NUM_BUFFERS) { - availableBuffers.push(buffer); - } - return; - } - - // Determine format based on channels - ALenum format = (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; - - // Upload audio data to OpenAL buffer - alBufferData(buffer, format, pcmBuffer.data(), - static_cast(samplesProcessed * sizeof(int16_t)), static_cast(sampleRate)); - CheckOpenALError("alBufferData"); - - // Queue the buffer - alSourceQueueBuffers(source, 1, &buffer); - CheckOpenALError("alSourceQueueBuffers"); - - // Track that we've queued this buffer - if (queuedBufferCount < NUM_BUFFERS) { - queuedBufferCount++; - } - - // Start playing if not already playing - ALint sourceState; - alGetSourcei(source, AL_SOURCE_STATE, &sourceState); - CheckOpenALError("alGetSourcei AL_SOURCE_STATE"); - - if (sourceState != AL_PLAYING) { - alSourcePlay(source); - CheckOpenALError("alSourcePlay"); - } - - playbackPosition += samplesProcessed / channels; - } - } +class OpenALAudioOutputDevice : public AudioOutputDevice +{ + public: + OpenALAudioOutputDevice() = default; + ~OpenALAudioOutputDevice() override + { + OpenALAudioOutputDevice::Stop(); + Cleanup(); + } + + bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) override + { + this->sampleRate = sampleRate; + this->channels = channels; + this->bufferSize = bufferSize; + + // Initialize OpenAL + device = alcOpenDevice(nullptr); // Use default device + if (!device) + { + std::cerr << "Failed to open OpenAL device" << std::endl; + return false; + } + + context = alcCreateContext(device, nullptr); + if (!context) + { + std::cerr << "Failed to create OpenAL context" << std::endl; + alcCloseDevice(device); + device = nullptr; + return false; + } + + if (!alcMakeContextCurrent(context)) + { + std::cerr << "Failed to make OpenAL context current" << std::endl; + alcDestroyContext(context); + alcCloseDevice(device); + context = nullptr; + device = nullptr; + return false; + } + + // Generate OpenAL source + alGenSources(1, &source); + CheckOpenALError("alGenSources"); + + // Generate OpenAL buffers for streaming + alGenBuffers(NUM_BUFFERS, buffers); + CheckOpenALError("alGenBuffers"); + + // Set source properties + alSourcef(source, AL_PITCH, 1.0f); + alSourcef(source, AL_GAIN, 1.0f); + alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + alSourcei(source, AL_LOOPING, AL_FALSE); + CheckOpenALError("Source setup"); + + // Initialize audio buffer + audioBuffer.resize(bufferSize * channels); + + // Initialize buffer tracking + queuedBufferCount = 0; + while (!availableBuffers.empty()) + { + availableBuffers.pop(); + } + + initialized = true; + return true; + } + + bool Start() override + { + if (!initialized) + { + std::cerr << "OpenAL audio output device not initialized" << std::endl; + return false; + } + + if (playing) + { + return true; // Already playing + } + + playing = true; + + // Start an audio playback thread + audioThread = std::thread(&OpenALAudioOutputDevice::AudioThreadFunction, this); + + return true; + } + + bool Stop() override + { + if (!playing) + { + return true; // Already stopped + } + + playing = false; + + // Wait for the audio thread to finish + if (audioThread.joinable()) + { + audioThread.join(); + } + + // Stop OpenAL source + if (initialized && source != 0) + { + alSourceStop(source); + CheckOpenALError("alSourceStop"); + } + + return true; + } + + bool WriteAudio(const float *data, uint32_t sampleCount) override + { + if (!initialized || !playing) + { + return false; + } + + std::lock_guard lock(bufferMutex); + + // Add audio data to the queue + for (uint32_t i = 0; i < sampleCount * channels; i++) + { + audioQueue.push(data[i]); + } + + return true; + } + + [[nodiscard]] bool IsPlaying() const override + { + return playing; + } + + [[nodiscard]] uint32_t GetPosition() const override + { + return playbackPosition; + } + + private: + static constexpr int NUM_BUFFERS = 8; + + uint32_t sampleRate = 44100; + uint32_t channels = 2; + uint32_t bufferSize = 1024; + bool initialized = false; + bool playing = false; + uint32_t playbackPosition = 0; + + // OpenAL objects + ALCdevice *device = nullptr; + ALCcontext *context = nullptr; + ALuint source = 0; + ALuint buffers[NUM_BUFFERS]{}; + int currentBuffer = 0; + + std::vector audioBuffer; + std::queue audioQueue; + std::mutex bufferMutex; + std::thread audioThread; + + // Buffer management for OpenAL streaming + std::queue availableBuffers; + int queuedBufferCount = 0; + + void Cleanup() + { + if (initialized) + { + // Clean up OpenAL resources + if (source != 0) + { + alDeleteSources(1, &source); + source = 0; + } + + alDeleteBuffers(NUM_BUFFERS, buffers); + + if (context) + { + alcMakeContextCurrent(nullptr); + alcDestroyContext(context); + context = nullptr; + } + + if (device) + { + alcCloseDevice(device); + device = nullptr; + } + + // Reset buffer tracking + queuedBufferCount = 0; + while (!availableBuffers.empty()) + { + availableBuffers.pop(); + } + + initialized = false; + } + } + + void AudioThreadFunction() + { + // Calculate sleep time for audio buffer updates (in milliseconds) + const auto sleepTime = std::chrono::milliseconds( + static_cast((bufferSize * 1000) / sampleRate / 8) // Eighth buffer time for responsiveness + ); + + while (playing) + { + ProcessAudioBuffer(); + std::this_thread::sleep_for(sleepTime); + } + } + + void ProcessAudioBuffer() + { + std::lock_guard lock(bufferMutex); + + // Fill audio buffer from queue in whole stereo frames to preserve channel alignment + uint32_t samplesProcessed = 0; + const uint32_t framesAvailable = static_cast(audioQueue.size() / channels); + if (framesAvailable == 0) + { + // Not enough data for a whole frame yet + return; + } + const uint32_t framesToSend = std::min(framesAvailable, bufferSize); + const uint32_t samplesToSend = framesToSend * channels; + for (uint32_t i = 0; i < samplesToSend; i++) + { + audioBuffer[i] = audioQueue.front(); + audioQueue.pop(); + } + samplesProcessed = samplesToSend; + + if (samplesProcessed > 0) + { + // Convert float samples to 16-bit PCM for OpenAL + std::vector pcmBuffer(samplesProcessed); + for (uint32_t i = 0; i < samplesProcessed; i++) + { + // Clamp and convert to 16-bit PCM + float sample = std::clamp(audioBuffer[i], -1.0f, 1.0f); + pcmBuffer[i] = static_cast(sample * 32767.0f); + } + + // Check for processed buffers and unqueue them + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + CheckOpenALError("alGetSourcei AL_BUFFERS_PROCESSED"); + + // Unqueue processed buffers and add them to available buffers + while (processed > 0) + { + ALuint buffer; + alSourceUnqueueBuffers(source, 1, &buffer); + CheckOpenALError("alSourceUnqueueBuffers"); + + // Add the unqueued buffer to available buffers + availableBuffers.push(buffer); + processed--; + } + + // Only proceed if we have an available buffer + ALuint buffer = 0; + if (!availableBuffers.empty()) + { + buffer = availableBuffers.front(); + availableBuffers.pop(); + } + else if (queuedBufferCount < NUM_BUFFERS) + { + // Use a buffer that hasn't been queued yet + buffer = buffers[queuedBufferCount]; + } + else + { + // No available buffers, skip this frame + return; + } + + // Validate buffer parameters + if (pcmBuffer.empty()) + { + // Re-add buffer to available list if we can't use it + if (queuedBufferCount >= NUM_BUFFERS) + { + availableBuffers.push(buffer); + } + return; + } + + // Determine format based on channels + ALenum format = (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + + // Upload audio data to OpenAL buffer + alBufferData(buffer, format, pcmBuffer.data(), + static_cast(samplesProcessed * sizeof(int16_t)), static_cast(sampleRate)); + CheckOpenALError("alBufferData"); + + // Queue the buffer + alSourceQueueBuffers(source, 1, &buffer); + CheckOpenALError("alSourceQueueBuffers"); + + // Track that we've queued this buffer + if (queuedBufferCount < NUM_BUFFERS) + { + queuedBufferCount++; + } + + // Start playing if not already playing + ALint sourceState; + alGetSourcei(source, AL_SOURCE_STATE, &sourceState); + CheckOpenALError("alGetSourcei AL_SOURCE_STATE"); + + if (sourceState != AL_PLAYING) + { + alSourcePlay(source); + CheckOpenALError("alSourcePlay"); + } + + playbackPosition += samplesProcessed / channels; + } + } }; -AudioSystem::~AudioSystem() { - // Stop the audio thread first - stopAudioThread(); +AudioSystem::~AudioSystem() +{ + // Stop the audio thread first + stopAudioThread(); - // Stop and clean up audio output device - if (outputDevice) { - outputDevice->Stop(); - outputDevice.reset(); - } + // Stop and clean up audio output device + if (outputDevice) + { + outputDevice->Stop(); + outputDevice.reset(); + } - // Destructor implementation - sources.clear(); - audioData.clear(); + // Destructor implementation + sources.clear(); + audioData.clear(); - // Clean up HRTF buffers - cleanupHRTFBuffers(); + // Clean up HRTF buffers + cleanupHRTFBuffers(); } -void AudioSystem::GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition) { - constexpr float sampleRate = 44100.0f; - const float frequency = 800.0f; // 800Hz ping - constexpr float pingDuration = 0.75f; // 0.75 second ping duration - constexpr auto pingSamples = static_cast(pingDuration * sampleRate); - constexpr float silenceDuration = 1.0f; // 1 second silence after ping - constexpr auto silenceSamples = static_cast(silenceDuration * sampleRate); - constexpr uint32_t totalCycleSamples = pingSamples + silenceSamples; - - const uint32_t attackSamples = static_cast(0.001f * sampleRate); // ~1ms attack - const uint32_t releaseSamples = static_cast(0.001f * sampleRate); // ~1ms release - constexpr float amplitude = 0.6f; - - for (uint32_t i = 0; i < sampleCount; i++) { - uint32_t globalPosition = playbackPosition + i; - uint32_t cyclePosition = globalPosition % totalCycleSamples; - - if (cyclePosition < pingSamples) { - float t = static_cast(cyclePosition) / sampleRate; - - // Minimal envelope for click prevention only - float envelope = 1.0f; - if (cyclePosition < attackSamples) { - envelope = static_cast(cyclePosition) / static_cast(std::max(1u, attackSamples)); - } else if (cyclePosition > pingSamples - releaseSamples) { - uint32_t relPos = pingSamples - cyclePosition; - envelope = static_cast(relPos) / static_cast(std::max(1u, releaseSamples)); - } - - float sineWave = sinf(2.0f * static_cast(M_PI) * frequency * t); - buffer[i] = amplitude * envelope * sineWave; - } else { - // Silence phase - buffer[i] = 0.0f; - } - } +void AudioSystem::GenerateSineWavePing(float *buffer, uint32_t sampleCount, uint32_t playbackPosition) +{ + constexpr float sampleRate = 44100.0f; + const float frequency = 800.0f; // 800Hz ping + constexpr float pingDuration = 0.75f; // 0.75 second ping duration + constexpr auto pingSamples = static_cast(pingDuration * sampleRate); + constexpr float silenceDuration = 1.0f; // 1 second silence after ping + constexpr auto silenceSamples = static_cast(silenceDuration * sampleRate); + constexpr uint32_t totalCycleSamples = pingSamples + silenceSamples; + + const uint32_t attackSamples = static_cast(0.001f * sampleRate); // ~1ms attack + const uint32_t releaseSamples = static_cast(0.001f * sampleRate); // ~1ms release + constexpr float amplitude = 0.6f; + + for (uint32_t i = 0; i < sampleCount; i++) + { + uint32_t globalPosition = playbackPosition + i; + uint32_t cyclePosition = globalPosition % totalCycleSamples; + + if (cyclePosition < pingSamples) + { + float t = static_cast(cyclePosition) / sampleRate; + + // Minimal envelope for click prevention only + float envelope = 1.0f; + if (cyclePosition < attackSamples) + { + envelope = static_cast(cyclePosition) / static_cast(std::max(1u, attackSamples)); + } + else if (cyclePosition > pingSamples - releaseSamples) + { + uint32_t relPos = pingSamples - cyclePosition; + envelope = static_cast(relPos) / static_cast(std::max(1u, releaseSamples)); + } + + float sineWave = sinf(2.0f * static_cast(M_PI) * frequency * t); + buffer[i] = amplitude * envelope * sineWave; + } + else + { + // Silence phase + buffer[i] = 0.0f; + } + } } -bool AudioSystem::Initialize(Engine* engine, Renderer* renderer) { - // Store the engine reference for accessing active camera - this->engine = engine; - - if (renderer) { - // Validate renderer if provided - if (!renderer->IsInitialized()) { - std::cerr << "AudioSystem::Initialize: Renderer is not initialized" << std::endl; - return false; - } - - // Store the renderer for compute shader support - this->renderer = renderer; - } else { - this->renderer = nullptr; - } - - // Generate default HRTF data for spatial audio processing - LoadHRTFData(""); // Pass empty filename to force generation of default HRTF data - - // Enable HRTF processing by default for 3D spatial audio - EnableHRTF(true); - - // Set default listener properties - SetListenerPosition(0.0f, 0.0f, 0.0f); - SetListenerOrientation(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); - SetListenerVelocity(0.0f, 0.0f, 0.0f); - SetMasterVolume(1.0f); - - // Initialize audio output device - outputDevice = std::make_unique(); - if (!outputDevice->Initialize(44100, 2, 1024)) { - std::cerr << "Failed to initialize audio output device" << std::endl; - return false; - } - - // Start audio output - if (!outputDevice->Start()) { - std::cerr << "Failed to start audio output device" << std::endl; - return false; - } - - // Start the background audio processing thread - startAudioThread(); - - initialized = true; - return true; +bool AudioSystem::Initialize(Engine *engine, Renderer *renderer) +{ + // Store the engine reference for accessing active camera + this->engine = engine; + + if (renderer) + { + // Validate renderer if provided + if (!renderer->IsInitialized()) + { + std::cerr << "AudioSystem::Initialize: Renderer is not initialized" << std::endl; + return false; + } + + // Store the renderer for compute shader support + this->renderer = renderer; + } + else + { + this->renderer = nullptr; + } + + // Generate default HRTF data for spatial audio processing + LoadHRTFData(""); // Pass empty filename to force generation of default HRTF data + + // Enable HRTF processing by default for 3D spatial audio + EnableHRTF(true); + + // Set default listener properties + SetListenerPosition(0.0f, 0.0f, 0.0f); + SetListenerOrientation(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); + SetListenerVelocity(0.0f, 0.0f, 0.0f); + SetMasterVolume(1.0f); + + // Initialize audio output device + outputDevice = std::make_unique(); + if (!outputDevice->Initialize(44100, 2, 1024)) + { + std::cerr << "Failed to initialize audio output device" << std::endl; + return false; + } + + // Start audio output + if (!outputDevice->Start()) + { + std::cerr << "Failed to start audio output device" << std::endl; + return false; + } + + // Start the background audio processing thread + startAudioThread(); + + initialized = true; + return true; } -void AudioSystem::Update(std::chrono::milliseconds deltaTime) { - if (!initialized) { - return; - } - - // Synchronize HRTF listener position and orientation with active camera - if (engine) { - const CameraComponent* activeCamera = engine->GetActiveCamera(); - if (activeCamera) { - // Get camera position - glm::vec3 cameraPos = activeCamera->GetPosition(); - SetListenerPosition(cameraPos.x, cameraPos.y, cameraPos.z); - - // Calculate camera forward and up vectors for orientation - // The camera looks at its target, so forward = normalize(target - position) - glm::vec3 target = activeCamera->GetTarget(); - glm::vec3 up = activeCamera->GetUp(); - glm::vec3 forward = glm::normalize(target - cameraPos); - - SetListenerOrientation(forward.x, forward.y, forward.z, up.x, up.y, up.z); - } - } - - // Update audio sources and process spatial audio - for (auto& source : sources) { - if (!source->IsPlaying()) { - continue; - } - - // Cast to ConcreteAudioSource to access timing methods - auto* concreteSource = dynamic_cast(source.get()); - - // Update playback timing and delay logic - concreteSource->UpdatePlayback(deltaTime, 0); - - // Only process audio if not in the delay phase - if (!concreteSource->ShouldProcessAudio()) { - continue; - } - - // Process audio with HRTF spatial processing (works with or without renderer) - if (hrtfEnabled && !hrtfData.empty()) { - // Get source position for spatial processing - const float* sourcePosition = concreteSource->GetPosition(); - - // Accumulate samples based on real time and process in fixed-size chunks to avoid tiny buffers - double acc = concreteSource->GetSampleAccumulator(); - acc += (static_cast(deltaTime.count()) * 44100.0) / 1000.0; // ms -> samples - constexpr uint32_t kChunk = 33075; - uint32_t available = static_cast(acc); - if (available < kChunk) { - // Not enough for a full chunk; keep accumulating - concreteSource->SetSampleAccumulator(acc); - continue; - } - // Process as many full chunks as available this frame - while (available >= kChunk) { - std::vector inputBuffer(kChunk, 0.0f); - std::vector outputBuffer(kChunk * 2, 0.0f); - uint32_t actualSamplesProcessed = 0; - - // Generate audio signal from loaded audio data or debug ping - auto audioIt = audioData.find(concreteSource->GetName()); - if (audioIt != audioData.end() && !audioIt->second.empty()) { - // Use actual loaded audio data with proper position tracking - const auto& data = audioIt->second; - uint32_t playbackPos = concreteSource->GetPlaybackPosition(); - - for (uint32_t i = 0; i < kChunk; i++) { - uint32_t dataIndex = (playbackPos + i) * 4; // 4 bytes per sample (16-bit stereo) - - if (dataIndex + 1 < data.size()) { - // Convert from 16-bit PCM to float - int16_t sample = *reinterpret_cast(&data[dataIndex]); - inputBuffer[i] = static_cast(sample) / 32768.0f; - actualSamplesProcessed++; - } else { - // Reached end of audio data - inputBuffer[i] = 0.0f; - } - } - } else { - // Generate sine wave ping for debugging - GenerateSineWavePing(inputBuffer.data(), kChunk, concreteSource->GetPlaybackPosition()); - actualSamplesProcessed = kChunk; - } - - // Build extended input [history | current] to preserve convolution continuity across chunks - uint32_t histLen = (hrtfSize > 0) ? (hrtfSize - 1) : 0; - static std::unordered_map> hrtfHistories; - auto &hist = hrtfHistories[concreteSource]; - if (hist.size() != histLen) { - hist.assign(histLen, 0.0f); - } - std::vector extendedInput(histLen + kChunk, 0.0f); - if (histLen > 0) { - std::memcpy(extendedInput.data(), hist.data(), histLen * sizeof(float)); - } - std::memcpy(extendedInput.data() + histLen, inputBuffer.data(), kChunk * sizeof(float)); - - // Submit for GPU HRTF processing via the background thread (trim will occur in processAudioTask) - submitAudioTask(extendedInput.data(), static_cast(extendedInput.size()), sourcePosition, actualSamplesProcessed, histLen); - - // Update history with the tail of current input - if (histLen > 0) { - std::memcpy(hist.data(), inputBuffer.data() + (kChunk - histLen), histLen * sizeof(float)); - } - - // Update playback timing with actual samples processed - concreteSource->UpdatePlayback(std::chrono::milliseconds(0), actualSamplesProcessed); - - // Consume one chunk from the accumulator - acc -= static_cast(kChunk); - available -= kChunk; - } - // Store fractional remainder for next frame - concreteSource->SetSampleAccumulator(acc); - } - } - - // Apply master volume changes to all active sources - for (auto& source : sources) { - if (source->IsPlaying()) { - // Master volume is applied during HRTF processing and individual source volume control - // Volume scaling is handled in the ProcessHRTF function - } - } - - // Clean up finished audio sources - std::erase_if(sources, - [](const std::unique_ptr& source) { - // Keep all sources active for continuous playback - // Audio sources can be stopped/started via their Play/Stop methods - return false; - }); - - // Update timing for audio processing with low-latency chunks - static std::chrono::milliseconds accumulatedTime = std::chrono::milliseconds(0); - accumulatedTime += deltaTime; - - // Process audio in 20ms chunks for optimal latency - constexpr std::chrono::milliseconds audioChunkTime = std::chrono::milliseconds(20); // 20ms chunks for real-time audio - if (accumulatedTime >= audioChunkTime) { - // Trigger audio buffer updates for smooth playback - // The HRTF processing ensures spatial audio is updated continuously - accumulatedTime = std::chrono::milliseconds(0); - - // Update listener properties if they have changed - // This ensures spatial audio positioning stays current with camera movement - } +void AudioSystem::Update(std::chrono::milliseconds deltaTime) +{ + if (!initialized) + { + return; + } + + // Synchronize HRTF listener position and orientation with active camera + if (engine) + { + const CameraComponent *activeCamera = engine->GetActiveCamera(); + if (activeCamera) + { + // Get camera position + glm::vec3 cameraPos = activeCamera->GetPosition(); + SetListenerPosition(cameraPos.x, cameraPos.y, cameraPos.z); + + // Calculate camera forward and up vectors for orientation + // The camera looks at its target, so forward = normalize(target - position) + glm::vec3 target = activeCamera->GetTarget(); + glm::vec3 up = activeCamera->GetUp(); + glm::vec3 forward = glm::normalize(target - cameraPos); + + SetListenerOrientation(forward.x, forward.y, forward.z, up.x, up.y, up.z); + } + } + + // Update audio sources and process spatial audio + for (auto &source : sources) + { + if (!source->IsPlaying()) + { + continue; + } + + // Cast to ConcreteAudioSource to access timing methods + auto *concreteSource = dynamic_cast(source.get()); + + // Update playback timing and delay logic + concreteSource->UpdatePlayback(deltaTime, 0); + + // Only process audio if not in the delay phase + if (!concreteSource->ShouldProcessAudio()) + { + continue; + } + + // Process audio with HRTF spatial processing (works with or without renderer) + if (hrtfEnabled && !hrtfData.empty()) + { + // Get source position for spatial processing + const float *sourcePosition = concreteSource->GetPosition(); + + // Accumulate samples based on real time and process in fixed-size chunks to avoid tiny buffers + double acc = concreteSource->GetSampleAccumulator(); + acc += (static_cast(deltaTime.count()) * 44100.0) / 1000.0; // ms -> samples + constexpr uint32_t kChunk = 33075; + uint32_t available = static_cast(acc); + if (available < kChunk) + { + // Not enough for a full chunk; keep accumulating + concreteSource->SetSampleAccumulator(acc); + continue; + } + // Process as many full chunks as available this frame + while (available >= kChunk) + { + std::vector inputBuffer(kChunk, 0.0f); + std::vector outputBuffer(kChunk * 2, 0.0f); + uint32_t actualSamplesProcessed = 0; + + // Generate audio signal from loaded audio data or debug ping + auto audioIt = audioData.find(concreteSource->GetName()); + if (audioIt != audioData.end() && !audioIt->second.empty()) + { + // Use actual loaded audio data with proper position tracking + const auto &data = audioIt->second; + uint32_t playbackPos = concreteSource->GetPlaybackPosition(); + + for (uint32_t i = 0; i < kChunk; i++) + { + uint32_t dataIndex = (playbackPos + i) * 4; // 4 bytes per sample (16-bit stereo) + + if (dataIndex + 1 < data.size()) + { + // Convert from 16-bit PCM to float + int16_t sample = *reinterpret_cast(&data[dataIndex]); + inputBuffer[i] = static_cast(sample) / 32768.0f; + actualSamplesProcessed++; + } + else + { + // Reached end of audio data + inputBuffer[i] = 0.0f; + } + } + } + else + { + // Generate sine wave ping for debugging + GenerateSineWavePing(inputBuffer.data(), kChunk, concreteSource->GetPlaybackPosition()); + actualSamplesProcessed = kChunk; + } + + // Build extended input [history | current] to preserve convolution continuity across chunks + uint32_t histLen = (hrtfSize > 0) ? (hrtfSize - 1) : 0; + static std::unordered_map> hrtfHistories; + auto &hist = hrtfHistories[concreteSource]; + if (hist.size() != histLen) + { + hist.assign(histLen, 0.0f); + } + std::vector extendedInput(histLen + kChunk, 0.0f); + if (histLen > 0) + { + std::memcpy(extendedInput.data(), hist.data(), histLen * sizeof(float)); + } + std::memcpy(extendedInput.data() + histLen, inputBuffer.data(), kChunk * sizeof(float)); + + // Submit for GPU HRTF processing via the background thread (trim will occur in processAudioTask) + submitAudioTask(extendedInput.data(), static_cast(extendedInput.size()), sourcePosition, actualSamplesProcessed, histLen); + + // Update history with the tail of current input + if (histLen > 0) + { + std::memcpy(hist.data(), inputBuffer.data() + (kChunk - histLen), histLen * sizeof(float)); + } + + // Update playback timing with actual samples processed + concreteSource->UpdatePlayback(std::chrono::milliseconds(0), actualSamplesProcessed); + + // Consume one chunk from the accumulator + acc -= static_cast(kChunk); + available -= kChunk; + } + // Store fractional remainder for next frame + concreteSource->SetSampleAccumulator(acc); + } + } + + // Apply master volume changes to all active sources + for (auto &source : sources) + { + if (source->IsPlaying()) + { + // Master volume is applied during HRTF processing and individual source volume control + // Volume scaling is handled in the ProcessHRTF function + } + } + + // Clean up finished audio sources + std::erase_if(sources, + [](const std::unique_ptr &source) { + // Keep all sources active for continuous playback + // Audio sources can be stopped/started via their Play/Stop methods + return false; + }); + + // Update timing for audio processing with low-latency chunks + static std::chrono::milliseconds accumulatedTime = std::chrono::milliseconds(0); + accumulatedTime += deltaTime; + + // Process audio in 20ms chunks for optimal latency + constexpr std::chrono::milliseconds audioChunkTime = std::chrono::milliseconds(20); // 20ms chunks for real-time audio + if (accumulatedTime >= audioChunkTime) + { + // Trigger audio buffer updates for smooth playback + // The HRTF processing ensures spatial audio is updated continuously + accumulatedTime = std::chrono::milliseconds(0); + + // Update listener properties if they have changed + // This ensures spatial audio positioning stays current with camera movement + } } -bool AudioSystem::LoadAudio(const std::string& filename, const std::string& name) { - - // Open the WAV file - std::ifstream file(filename, std::ios::binary); - if (!file.is_open()) { - std::cerr << "Failed to open audio file: " << filename << std::endl; - return false; - } - - // Read WAV header - struct WAVHeader { - char riff[4]; // "RIFF" - uint32_t fileSize; // File size - 8 - char wave[4]; // "WAVE" - char fmt[4]; // "fmt " - uint32_t fmtSize; // Format chunk size - uint16_t audioFormat; // Audio format (1 = PCM) - uint16_t numChannels; // Number of channels - uint32_t sampleRate; // Sample rate - uint32_t byteRate; // Byte rate - uint16_t blockAlign; // Block align - uint16_t bitsPerSample; // Bits per sample - char data[4]; // "data" - uint32_t dataSize; // Data size - }; - - WAVHeader header{}; - file.read(reinterpret_cast(&header), sizeof(WAVHeader)); - - // Validate WAV header - if (std::strncmp(header.riff, "RIFF", 4) != 0 || - std::strncmp(header.wave, "WAVE", 4) != 0 || - std::strncmp(header.fmt, "fmt ", 4) != 0 || - std::strncmp(header.data, "data", 4) != 0) { - std::cerr << "Invalid WAV file format: " << filename << std::endl; - file.close(); - return false; - } - - // Only support PCM format for now - if (header.audioFormat != 1) { - std::cerr << "Unsupported audio format (only PCM supported): " << filename << std::endl; - file.close(); - return false; - } - - // Read audio data - std::vector data(header.dataSize); - file.read(reinterpret_cast(data.data()), header.dataSize); - file.close(); - - if (file.gcount() != static_cast(header.dataSize)) { - std::cerr << "Failed to read complete audio data from: " << filename << std::endl; - return false; - } - - // Store the audio data - audioData[name] = std::move(data); - - return true; +bool AudioSystem::LoadAudio(const std::string &filename, const std::string &name) +{ + // Open the WAV file + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) + { + std::cerr << "Failed to open audio file: " << filename << std::endl; + return false; + } + + // Read WAV header + struct WAVHeader + { + char riff[4]; // "RIFF" + uint32_t fileSize; // File size - 8 + char wave[4]; // "WAVE" + char fmt[4]; // "fmt " + uint32_t fmtSize; // Format chunk size + uint16_t audioFormat; // Audio format (1 = PCM) + uint16_t numChannels; // Number of channels + uint32_t sampleRate; // Sample rate + uint32_t byteRate; // Byte rate + uint16_t blockAlign; // Block align + uint16_t bitsPerSample; // Bits per sample + char data[4]; // "data" + uint32_t dataSize; // Data size + }; + + WAVHeader header{}; + file.read(reinterpret_cast(&header), sizeof(WAVHeader)); + + // Validate WAV header + if (std::strncmp(header.riff, "RIFF", 4) != 0 || + std::strncmp(header.wave, "WAVE", 4) != 0 || + std::strncmp(header.fmt, "fmt ", 4) != 0 || + std::strncmp(header.data, "data", 4) != 0) + { + std::cerr << "Invalid WAV file format: " << filename << std::endl; + file.close(); + return false; + } + + // Only support PCM format for now + if (header.audioFormat != 1) + { + std::cerr << "Unsupported audio format (only PCM supported): " << filename << std::endl; + file.close(); + return false; + } + + // Read audio data + std::vector data(header.dataSize); + file.read(reinterpret_cast(data.data()), header.dataSize); + file.close(); + + if (file.gcount() != static_cast(header.dataSize)) + { + std::cerr << "Failed to read complete audio data from: " << filename << std::endl; + return false; + } + + // Store the audio data + audioData[name] = std::move(data); + + return true; } -AudioSource* AudioSystem::CreateAudioSource(const std::string& name) { - // Check if the audio data exists - auto it = audioData.find(name); - if (it == audioData.end()) { - std::cerr << "AudioSystem::CreateAudioSource: Audio data not found: " << name << std::endl; - return nullptr; - } - - // Create a new audio source - auto source = std::make_unique(name); - - // Calculate audio length in samples for timing - const auto& data = it->second; - if (!data.empty()) { - // Assuming 16-bit stereo audio at 44.1kHz (standard WAV format) - // The audio data reading uses dataIndex = (playbackPos + i) * 4 - // So we need to calculate length based on how many individual samples we can read - // Each 4 bytes represents one stereo sample pair, so total individual samples = data.size() / 4 - uint32_t totalSamples = static_cast(data.size()) / 4; - - // Set the audio length for proper timing - source->SetAudioLength(totalSamples); - } - - // Store the source - sources.push_back(std::move(source)); - - return sources.back().get(); +AudioSource *AudioSystem::CreateAudioSource(const std::string &name) +{ + // Check if the audio data exists + auto it = audioData.find(name); + if (it == audioData.end()) + { + std::cerr << "AudioSystem::CreateAudioSource: Audio data not found: " << name << std::endl; + return nullptr; + } + + // Create a new audio source + auto source = std::make_unique(name); + + // Calculate audio length in samples for timing + const auto &data = it->second; + if (!data.empty()) + { + // Assuming 16-bit stereo audio at 44.1kHz (standard WAV format) + // The audio data reading uses dataIndex = (playbackPos + i) * 4 + // So we need to calculate length based on how many individual samples we can read + // Each 4 bytes represents one stereo sample pair, so total individual samples = data.size() / 4 + uint32_t totalSamples = static_cast(data.size()) / 4; + + // Set the audio length for proper timing + source->SetAudioLength(totalSamples); + } + + // Store the source + sources.push_back(std::move(source)); + + return sources.back().get(); } -AudioSource* AudioSystem::CreateDebugPingSource(const std::string& name) { - // Create a new audio source for debugging - auto source = std::make_unique(name); +AudioSource *AudioSystem::CreateDebugPingSource(const std::string &name) +{ + // Create a new audio source for debugging + auto source = std::make_unique(name); - // Set up debug ping parameters - // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) - constexpr float sampleRate = 44100.0f; - constexpr float pingDuration = 0.5f; - constexpr float silenceDuration = 1.0f; - constexpr auto totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); + // Set up debug ping parameters + // The ping will cycle every 1.5 seconds (0.5s ping + 1.0s silence) + constexpr float sampleRate = 44100.0f; + constexpr float pingDuration = 0.5f; + constexpr float silenceDuration = 1.0f; + constexpr auto totalCycleSamples = static_cast((pingDuration + silenceDuration) * sampleRate); - // For generated ping, let the generator control the 0.5s ping + 1.0s silence cycle. - // Disable source-level length/delay to avoid double-silence and audible resets. - source->SetAudioLength(0); + // For generated ping, let the generator control the 0.5s ping + 1.0s silence cycle. + // Disable source-level length/delay to avoid double-silence and audible resets. + source->SetAudioLength(0); - // Store the source - sources.push_back(std::move(source)); + // Store the source + sources.push_back(std::move(source)); - return sources.back().get(); + return sources.back().get(); } -void AudioSystem::SetListenerPosition(const float x, const float y, const float z) { - listenerPosition[0] = x; - listenerPosition[1] = y; - listenerPosition[2] = z; +void AudioSystem::SetListenerPosition(const float x, const float y, const float z) +{ + listenerPosition[0] = x; + listenerPosition[1] = y; + listenerPosition[2] = z; } void AudioSystem::SetListenerOrientation(const float forwardX, const float forwardY, const float forwardZ, - const float upX, const float upY, const float upZ) { - listenerOrientation[0] = forwardX; - listenerOrientation[1] = forwardY; - listenerOrientation[2] = forwardZ; - listenerOrientation[3] = upX; - listenerOrientation[4] = upY; - listenerOrientation[5] = upZ; + const float upX, const float upY, const float upZ) +{ + listenerOrientation[0] = forwardX; + listenerOrientation[1] = forwardY; + listenerOrientation[2] = forwardZ; + listenerOrientation[3] = upX; + listenerOrientation[4] = upY; + listenerOrientation[5] = upZ; } -void AudioSystem::SetListenerVelocity(const float x, const float y, const float z) { - listenerVelocity[0] = x; - listenerVelocity[1] = y; - listenerVelocity[2] = z; +void AudioSystem::SetListenerVelocity(const float x, const float y, const float z) +{ + listenerVelocity[0] = x; + listenerVelocity[1] = y; + listenerVelocity[2] = z; } -void AudioSystem::SetMasterVolume(const float volume) { - masterVolume = volume; +void AudioSystem::SetMasterVolume(const float volume) +{ + masterVolume = volume; } -void AudioSystem::EnableHRTF(const bool enable) { - hrtfEnabled = enable; +void AudioSystem::EnableHRTF(const bool enable) +{ + hrtfEnabled = enable; } -bool AudioSystem::IsHRTFEnabled() const { - return hrtfEnabled; +bool AudioSystem::IsHRTFEnabled() const +{ + return hrtfEnabled; } -void AudioSystem::SetHRTFCPUOnly(const bool cpuOnly) { - (void)cpuOnly; - // Enforce GPU-only HRTF processing: ignore CPU-only requests - hrtfCPUOnly = false; +void AudioSystem::SetHRTFCPUOnly(const bool cpuOnly) +{ + (void) cpuOnly; + // Enforce GPU-only HRTF processing: ignore CPU-only requests + hrtfCPUOnly = false; } -bool AudioSystem::IsHRTFCPUOnly() const { - return hrtfCPUOnly; +bool AudioSystem::IsHRTFCPUOnly() const +{ + return hrtfCPUOnly; } -bool AudioSystem::LoadHRTFData(const std::string& filename) { - - // HRTF parameters - constexpr uint32_t hrtfSampleCount = 256; // Number of samples per impulse response - constexpr uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) - constexpr uint32_t channelCount = 2; // Stereo (left and right ears) - const float sampleRate = 44100.0f; // Sample rate for HRTF data - const float speedOfSound = 343.0f; // Speed of sound in m/s - const float headRadius = 0.0875f; // Average head radius in meters - - // Try to load from a file first (only if the filename is provided) - if (!filename.empty()) { - if (std::ifstream file(filename, std::ios::binary); file.is_open()) { - // Read the file header to determine a format - char header[4]; - file.read(header, 4); - - if (std::strncmp(header, "HRTF", 4) == 0) { - // Custom HRTF format - uint32_t fileHrtfSize, filePositionCount, fileChannelCount; - file.read(reinterpret_cast(&fileHrtfSize), sizeof(uint32_t)); - file.read(reinterpret_cast(&filePositionCount), sizeof(uint32_t)); - file.read(reinterpret_cast(&fileChannelCount), sizeof(uint32_t)); - - if (fileChannelCount == channelCount) { - hrtfData.resize(fileHrtfSize * filePositionCount * fileChannelCount); - file.read(reinterpret_cast(hrtfData.data()), static_cast(hrtfData.size() * sizeof(float))); - - hrtfSize = fileHrtfSize; - numHrtfPositions = filePositionCount; - - file.close(); - return true; - } - } - file.close(); - } - } - - // Generate realistic HRTF data based on acoustic modeling - // Resize the HRTF data vector - hrtfData.resize(hrtfSampleCount * positionCount * channelCount); - - // Generate HRTF impulse responses for each position - for (uint32_t pos = 0; pos < positionCount; pos++) { - // Calculate azimuth and elevation for this position - uint32_t azimuthIndex = pos % 36; - uint32_t elevationIndex = pos / 36; - - float azimuth = (static_cast(azimuthIndex) * 10.0f - 180.0f) * static_cast(M_PI) / 180.0f; - float elevation = (static_cast(elevationIndex) * 15.0f - 90.0f) * static_cast(M_PI) / 180.0f; - - // Convert to Cartesian coordinates - float x = std::cos(elevation) * std::sin(azimuth); - float y = std::sin(elevation); - float z = std::cos(elevation) * std::cos(azimuth); - - for (uint32_t channel = 0; channel < channelCount; channel++) { - // Calculate ear position (left ear: -0.1m, right ear: +0.1m on x-axis) - float earX = (channel == 0) ? -0.1f : 0.1f; - - // Calculate distance from source to ear - float dx = x - earX; - float dy = y; - float dz = z; - float distance = std::sqrt(dx * dx + dy * dy + dz * dz); - - // Calculate time delay (ITD - Interaural Time Difference) - float timeDelay = distance / speedOfSound; - auto sampleDelay = static_cast(timeDelay * sampleRate); - - // Calculate head shadow effect (ILD - Interaural Level Difference) - float shadowFactor = 1.0f; - if (channel == 0 && azimuth > 0) { // Left ear, source on right - shadowFactor = 0.3f + 0.7f * std::exp(-azimuth * 2.0f); - } else if (channel == 1 && azimuth < 0) { // Right ear, source on left - shadowFactor = 0.3f + 0.7f * std::exp(azimuth * 2.0f); - } - - - // Generate impulse response - uint32_t samplesGenerated = 0; - for (uint32_t i = 0; i < hrtfSampleCount; i++) { - float value = 0.0f; - - // Direct path impulse - if (i >= sampleDelay && i < sampleDelay + 10) { - float t = static_cast(i - sampleDelay) / sampleRate; - value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * static_cast(M_PI) * 1000.0f * t); - } - - - // Apply distance attenuation - value /= std::max(1.0f, distance); - - uint32_t index = pos * hrtfSampleCount * channelCount + channel * hrtfSampleCount + i; - hrtfData[index] = value; - } - } - } - - // Store HRTF parameters - hrtfSize = hrtfSampleCount; - numHrtfPositions = positionCount; - - return true; +bool AudioSystem::LoadHRTFData(const std::string &filename) +{ + // HRTF parameters + constexpr uint32_t hrtfSampleCount = 256; // Number of samples per impulse response + constexpr uint32_t positionCount = 36 * 13; // 36 azimuths (10-degree steps) * 13 elevations (15-degree steps) + constexpr uint32_t channelCount = 2; // Stereo (left and right ears) + const float sampleRate = 44100.0f; // Sample rate for HRTF data + const float speedOfSound = 343.0f; // Speed of sound in m/s + const float headRadius = 0.0875f; // Average head radius in meters + + // Try to load from a file first (only if the filename is provided) + if (!filename.empty()) + { + if (std::ifstream file(filename, std::ios::binary); file.is_open()) + { + // Read the file header to determine a format + char header[4]; + file.read(header, 4); + + if (std::strncmp(header, "HRTF", 4) == 0) + { + // Custom HRTF format + uint32_t fileHrtfSize, filePositionCount, fileChannelCount; + file.read(reinterpret_cast(&fileHrtfSize), sizeof(uint32_t)); + file.read(reinterpret_cast(&filePositionCount), sizeof(uint32_t)); + file.read(reinterpret_cast(&fileChannelCount), sizeof(uint32_t)); + + if (fileChannelCount == channelCount) + { + hrtfData.resize(fileHrtfSize * filePositionCount * fileChannelCount); + file.read(reinterpret_cast(hrtfData.data()), static_cast(hrtfData.size() * sizeof(float))); + + hrtfSize = fileHrtfSize; + numHrtfPositions = filePositionCount; + + file.close(); + return true; + } + } + file.close(); + } + } + + // Generate realistic HRTF data based on acoustic modeling + // Resize the HRTF data vector + hrtfData.resize(hrtfSampleCount * positionCount * channelCount); + + // Generate HRTF impulse responses for each position + for (uint32_t pos = 0; pos < positionCount; pos++) + { + // Calculate azimuth and elevation for this position + uint32_t azimuthIndex = pos % 36; + uint32_t elevationIndex = pos / 36; + + float azimuth = (static_cast(azimuthIndex) * 10.0f - 180.0f) * static_cast(M_PI) / 180.0f; + float elevation = (static_cast(elevationIndex) * 15.0f - 90.0f) * static_cast(M_PI) / 180.0f; + + // Convert to Cartesian coordinates + float x = std::cos(elevation) * std::sin(azimuth); + float y = std::sin(elevation); + float z = std::cos(elevation) * std::cos(azimuth); + + for (uint32_t channel = 0; channel < channelCount; channel++) + { + // Calculate ear position (left ear: -0.1m, right ear: +0.1m on x-axis) + float earX = (channel == 0) ? -0.1f : 0.1f; + + // Calculate distance from source to ear + float dx = x - earX; + float dy = y; + float dz = z; + float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + // Calculate time delay (ITD - Interaural Time Difference) + float timeDelay = distance / speedOfSound; + auto sampleDelay = static_cast(timeDelay * sampleRate); + + // Calculate head shadow effect (ILD - Interaural Level Difference) + float shadowFactor = 1.0f; + if (channel == 0 && azimuth > 0) + { // Left ear, source on right + shadowFactor = 0.3f + 0.7f * std::exp(-azimuth * 2.0f); + } + else if (channel == 1 && azimuth < 0) + { // Right ear, source on left + shadowFactor = 0.3f + 0.7f * std::exp(azimuth * 2.0f); + } + + // Generate impulse response + uint32_t samplesGenerated = 0; + for (uint32_t i = 0; i < hrtfSampleCount; i++) + { + float value = 0.0f; + + // Direct path impulse + if (i >= sampleDelay && i < sampleDelay + 10) + { + float t = static_cast(i - sampleDelay) / sampleRate; + value = shadowFactor * std::exp(-t * 1000.0f) * std::cos(2.0f * static_cast(M_PI) * 1000.0f * t); + } + + // Apply distance attenuation + value /= std::max(1.0f, distance); + + uint32_t index = pos * hrtfSampleCount * channelCount + channel * hrtfSampleCount + i; + hrtfData[index] = value; + } + } + } + + // Store HRTF parameters + hrtfSize = hrtfSampleCount; + numHrtfPositions = positionCount; + + return true; } -bool AudioSystem::ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition) { - - if (!hrtfEnabled) { - // If HRTF is disabled, just copy input to output - for (uint32_t i = 0; i < sampleCount; i++) { - outputBuffer[i * 2] = inputBuffer[i]; // Left channel - outputBuffer[i * 2 + 1] = inputBuffer[i]; // Right channel - } - return true; - } - - // Check if we should use CPU-only processing or if Vulkan is not available - // Also force CPU processing if we've detected threading issues previously - static bool forceGPUFallback = false; - if (hrtfCPUOnly || !renderer || !renderer->IsInitialized() || forceGPUFallback) { - // Use CPU-based HRTF processing (either forced or fallback) - - // Create buffers for HRTF processing if they don't exist or if the sample count has changed - if (!createHRTFBuffers(sampleCount)) { - std::cerr << "Failed to create HRTF buffers" << std::endl; - return false; - } - - // Copy input data to input buffer - void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); - memcpy(data, inputBuffer, sampleCount * sizeof(float)); - inputBufferMemory.unmapMemory(); - - // Copy source and listener positions - memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); - memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); - memcpy(params.listenerOrientation, listenerOrientation, sizeof(float) * 6); - params.sampleCount = sampleCount; - params.hrtfSize = hrtfSize; - params.numHrtfPositions = numHrtfPositions; - params.padding = 0.0f; - - // Copy parameters to parameter buffer using persistent memory mapping - if (persistentParamsMemory) { - memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); - } else { - std::cerr << "WARNING: Persistent memory not available, falling back to map/unmap" << std::endl; - data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); - memcpy(data, ¶ms, sizeof(HRTFParams)); - paramsBufferMemory.unmapMemory(); - } - - // Perform HRTF processing using CPU-based convolution - // This implementation provides real-time 3D audio spatialization - - // Calculate direction from listener to source - float direction[3]; - direction[0] = sourcePosition[0] - listenerPosition[0]; - direction[1] = sourcePosition[1] - listenerPosition[1]; - direction[2] = sourcePosition[2] - listenerPosition[2]; - - // Normalize direction - float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); - if (length > 0.0001f) { - direction[0] /= length; - direction[1] /= length; - direction[2] /= length; - } else { - direction[0] = 0.0f; - direction[1] = 0.0f; - direction[2] = -1.0f; // Default to front - } - - // Calculate azimuth and elevation - float azimuth = std::atan2(direction[0], direction[2]); - float elevation = std::asin(std::max(-1.0f, std::min(1.0f, direction[1]))); - - // Convert to indices - int azimuthIndex = static_cast((azimuth + M_PI) / (2.0f * M_PI) * 36.0f) % 36; - int elevationIndex = static_cast((elevation + M_PI / 2.0f) / M_PI * 13.0f); - elevationIndex = std::max(0, std::min(12, elevationIndex)); - - // Get HRTF index - int hrtfIndex = elevationIndex * 36 + azimuthIndex; - hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); - - // Perform convolution for left and right ears with simple overlap-add using per-direction input history - static std::unordered_map> convHistories; // mono histories keyed by hrtfIndex - const uint32_t histLenDesired = (hrtfSize > 0) ? (hrtfSize - 1) : 0; - auto &convHistory = convHistories[hrtfIndex]; - if (convHistory.size() != histLenDesired) { - convHistory.assign(histLenDesired, 0.0f); - } - - // Build extended input: [history | current input] - std::vector extInput(histLenDesired + sampleCount, 0.0f); - if (histLenDesired > 0) { - std::memcpy(extInput.data(), convHistory.data(), histLenDesired * sizeof(float)); - } - if (sampleCount > 0) { - std::memcpy(extInput.data() + histLenDesired, inputBuffer, sampleCount * sizeof(float)); - } - - for (uint32_t i = 0; i < sampleCount; i++) { - float leftSample = 0.0f; - float rightSample = 0.0f; - - // Convolve with HRTF impulse response using extended input - // extIndex = histLenDesired + i - j; ensure extIndex >= 0 - uint32_t jMax = std::min(hrtfSize - 1, histLenDesired + i); - for (uint32_t j = 0; j <= jMax; j++) { - uint32_t extIndex = histLenDesired + i - j; - uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; - uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; - - if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) { - float in = extInput[extIndex]; - leftSample += in * hrtfData[hrtfLeftIndex]; - rightSample += in * hrtfData[hrtfRightIndex]; - } - } - - // Apply distance attenuation - float distanceAttenuation = 1.0f / std::max(1.0f, length); - leftSample *= distanceAttenuation; - rightSample *= distanceAttenuation; - - // Write to output buffer - outputBuffer[i * 2] = leftSample; - outputBuffer[i * 2 + 1] = rightSample; - } - - // Update history with the tail of the extended input - if (histLenDesired > 0) { - std::memcpy(convHistory.data(), extInput.data() + sampleCount, histLenDesired * sizeof(float)); - } - - - - return true; - } else { - // Use Vulkan shader-based HRTF processing with fallback to CPU - try { - // Validate HRTF data exists - if (hrtfData.empty()) { - LoadHRTFData(""); // Generate HRTF data - } - - // Create buffers for HRTF processing if they don't exist or if the sample count has changed - if (!createHRTFBuffers(sampleCount)) { - std::cerr << "Failed to create HRTF buffers, falling back to CPU processing" << std::endl; - throw std::runtime_error("Buffer creation failed"); - } - - // Copy input data to input buffer - void* data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); - memcpy(data, inputBuffer, sampleCount * sizeof(float)); - - - inputBufferMemory.unmapMemory(); - - // Set up HRTF parameters with proper std140 uniform buffer layout - struct alignas(16) HRTFParams { - float listenerPosition[4]; // vec3 + padding (16 bytes) - offset 0 - float listenerForward[4]; // vec3 + padding (16 bytes) - offset 16 - float listenerUp[4]; // vec3 + padding (16 bytes) - offset 32 - float sourcePosition[4]; // vec3 + padding (16 bytes) - offset 48 - float sampleCount; // float (4 bytes) - offset 64 - float padding1[3]; // Padding to align to 16-byte boundary - offset 68 - uint32_t inputChannels; // uint (4 bytes) - offset 80 - uint32_t outputChannels; // uint (4 bytes) - offset 84 - uint32_t hrtfSize; // uint (4 bytes) - offset 88 - uint32_t numHrtfPositions; // uint (4 bytes) - offset 92 - float distanceAttenuation; // float (4 bytes) - offset 96 - float dopplerFactor; // float (4 bytes) - offset 100 - float reverbMix; // float (4 bytes) - offset 104 - float padding2; // Padding to complete 16-byte alignment - offset 108 - } params{}; - - // Copy listener and source positions with proper padding for GPU alignment - memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); - params.listenerPosition[3] = 0.0f; // Padding for float3 alignment - memcpy(params.listenerForward, &listenerOrientation[0], sizeof(float) * 3); // Forward vector - params.listenerForward[3] = 0.0f; // Padding for float3 alignment - memcpy(params.listenerUp, &listenerOrientation[3], sizeof(float) * 3); // Up vector - params.listenerUp[3] = 0.0f; // Padding for float3 alignment - memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); - params.sourcePosition[3] = 0.0f; // Padding for float3 alignment - params.sampleCount = static_cast(sampleCount); // Number of samples to process - params.padding1[0] = params.padding1[1] = params.padding1[2] = 0.0f; // Initialize padding - params.inputChannels = 1; // Mono input - params.outputChannels = 2; // Stereo output - params.hrtfSize = hrtfSize; - params.numHrtfPositions = numHrtfPositions; - params.distanceAttenuation = 1.0f; - params.dopplerFactor = 1.0f; - params.reverbMix = 0.0f; - params.padding2 = 0.0f; // Initialize padding - - // Copy parameters to parameter buffer using persistent memory mapping - if (persistentParamsMemory) { - memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); - } else { - std::cerr << "ERROR: Persistent memory not available for GPU processing!" << std::endl; - throw std::runtime_error("Persistent memory required for GPU processing"); - } - - - // Use renderer's main compute pipeline instead of dedicated HRTF pipeline - uint32_t workGroupSize = 64; // Must match the numthreads in the shader - uint32_t groupCountX = (sampleCount + workGroupSize - 1) / workGroupSize; - - - - // Use renderer's main compute pipeline dispatch method - auto computeFence = renderer->DispatchCompute(groupCountX, 1, 1, - *this->inputBuffer, *this->outputBuffer, - *this->hrtfBuffer, *this->paramsBuffer); - - // Wait for compute shader to complete using fence-based synchronization - const vk::raii::Device& device = renderer->GetRaiiDevice(); - vk::Result result = device.waitForFences(*computeFence, VK_TRUE, UINT64_MAX); - if (result != vk::Result::eSuccess) { - std::cerr << "Failed to wait for compute fence: " << vk::to_string(result) << std::endl; - throw std::runtime_error("Fence wait failed"); - } - - - // Copy results from output buffer to the output array - void* outputData = outputBufferMemory.mapMemory(0, sampleCount * 2 * sizeof(float)); - - - memcpy(outputBuffer, outputData, sampleCount * 2 * sizeof(float)); - outputBufferMemory.unmapMemory(); - - - - return true; - } catch (const std::exception& e) { - std::cerr << "GPU HRTF processing failed: " << e.what() << std::endl; - std::cerr << "CPU fallback disabled - GPU path required" << std::endl; - throw; // Re-throw the exception to ensure failure without CPU fallback - } - } +bool AudioSystem::ProcessHRTF(const float *inputBuffer, float *outputBuffer, uint32_t sampleCount, const float *sourcePosition) +{ + if (!hrtfEnabled) + { + // If HRTF is disabled, just copy input to output + for (uint32_t i = 0; i < sampleCount; i++) + { + outputBuffer[i * 2] = inputBuffer[i]; // Left channel + outputBuffer[i * 2 + 1] = inputBuffer[i]; // Right channel + } + return true; + } + + // Check if we should use CPU-only processing or if Vulkan is not available + // Also force CPU processing if we've detected threading issues previously + static bool forceGPUFallback = false; + if (hrtfCPUOnly || !renderer || !renderer->IsInitialized() || forceGPUFallback) + { + // Use CPU-based HRTF processing (either forced or fallback) + + // Create buffers for HRTF processing if they don't exist or if the sample count has changed + if (!createHRTFBuffers(sampleCount)) + { + std::cerr << "Failed to create HRTF buffers" << std::endl; + return false; + } + + // Copy input data to input buffer + void *data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); + memcpy(data, inputBuffer, sampleCount * sizeof(float)); + inputBufferMemory.unmapMemory(); + + // Copy source and listener positions + memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); + memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); + memcpy(params.listenerOrientation, listenerOrientation, sizeof(float) * 6); + params.sampleCount = sampleCount; + params.hrtfSize = hrtfSize; + params.numHrtfPositions = numHrtfPositions; + params.padding = 0.0f; + + // Copy parameters to parameter buffer using persistent memory mapping + if (persistentParamsMemory) + { + memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); + } + else + { + std::cerr << "WARNING: Persistent memory not available, falling back to map/unmap" << std::endl; + data = paramsBufferMemory.mapMemory(0, sizeof(HRTFParams)); + memcpy(data, ¶ms, sizeof(HRTFParams)); + paramsBufferMemory.unmapMemory(); + } + + // Perform HRTF processing using CPU-based convolution + // This implementation provides real-time 3D audio spatialization + + // Calculate direction from listener to source + float direction[3]; + direction[0] = sourcePosition[0] - listenerPosition[0]; + direction[1] = sourcePosition[1] - listenerPosition[1]; + direction[2] = sourcePosition[2] - listenerPosition[2]; + + // Normalize direction + float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); + if (length > 0.0001f) + { + direction[0] /= length; + direction[1] /= length; + direction[2] /= length; + } + else + { + direction[0] = 0.0f; + direction[1] = 0.0f; + direction[2] = -1.0f; // Default to front + } + + // Calculate azimuth and elevation + float azimuth = std::atan2(direction[0], direction[2]); + float elevation = std::asin(std::max(-1.0f, std::min(1.0f, direction[1]))); + + // Convert to indices + int azimuthIndex = static_cast((azimuth + M_PI) / (2.0f * M_PI) * 36.0f) % 36; + int elevationIndex = static_cast((elevation + M_PI / 2.0f) / M_PI * 13.0f); + elevationIndex = std::max(0, std::min(12, elevationIndex)); + + // Get HRTF index + int hrtfIndex = elevationIndex * 36 + azimuthIndex; + hrtfIndex = std::min(hrtfIndex, static_cast(numHrtfPositions) - 1); + + // Perform convolution for left and right ears with simple overlap-add using per-direction input history + static std::unordered_map> convHistories; // mono histories keyed by hrtfIndex + const uint32_t histLenDesired = (hrtfSize > 0) ? (hrtfSize - 1) : 0; + auto &convHistory = convHistories[hrtfIndex]; + if (convHistory.size() != histLenDesired) + { + convHistory.assign(histLenDesired, 0.0f); + } + + // Build extended input: [history | current input] + std::vector extInput(histLenDesired + sampleCount, 0.0f); + if (histLenDesired > 0) + { + std::memcpy(extInput.data(), convHistory.data(), histLenDesired * sizeof(float)); + } + if (sampleCount > 0) + { + std::memcpy(extInput.data() + histLenDesired, inputBuffer, sampleCount * sizeof(float)); + } + + for (uint32_t i = 0; i < sampleCount; i++) + { + float leftSample = 0.0f; + float rightSample = 0.0f; + + // Convolve with HRTF impulse response using extended input + // extIndex = histLenDesired + i - j; ensure extIndex >= 0 + uint32_t jMax = std::min(hrtfSize - 1, histLenDesired + i); + for (uint32_t j = 0; j <= jMax; j++) + { + uint32_t extIndex = histLenDesired + i - j; + uint32_t hrtfLeftIndex = hrtfIndex * hrtfSize * 2 + j; + uint32_t hrtfRightIndex = hrtfIndex * hrtfSize * 2 + hrtfSize + j; + + if (hrtfLeftIndex < hrtfData.size() && hrtfRightIndex < hrtfData.size()) + { + float in = extInput[extIndex]; + leftSample += in * hrtfData[hrtfLeftIndex]; + rightSample += in * hrtfData[hrtfRightIndex]; + } + } + + // Apply distance attenuation + float distanceAttenuation = 1.0f / std::max(1.0f, length); + leftSample *= distanceAttenuation; + rightSample *= distanceAttenuation; + + // Write to output buffer + outputBuffer[i * 2] = leftSample; + outputBuffer[i * 2 + 1] = rightSample; + } + + // Update history with the tail of the extended input + if (histLenDesired > 0) + { + std::memcpy(convHistory.data(), extInput.data() + sampleCount, histLenDesired * sizeof(float)); + } + + return true; + } + else + { + // Use Vulkan shader-based HRTF processing with fallback to CPU + try + { + // Validate HRTF data exists + if (hrtfData.empty()) + { + LoadHRTFData(""); // Generate HRTF data + } + + // Create buffers for HRTF processing if they don't exist or if the sample count has changed + if (!createHRTFBuffers(sampleCount)) + { + std::cerr << "Failed to create HRTF buffers, falling back to CPU processing" << std::endl; + throw std::runtime_error("Buffer creation failed"); + } + + // Copy input data to input buffer + void *data = inputBufferMemory.mapMemory(0, sampleCount * sizeof(float)); + memcpy(data, inputBuffer, sampleCount * sizeof(float)); + + inputBufferMemory.unmapMemory(); + + // Set up HRTF parameters with proper std140 uniform buffer layout + struct alignas(16) HRTFParams + { + float listenerPosition[4]; // vec3 + padding (16 bytes) - offset 0 + float listenerForward[4]; // vec3 + padding (16 bytes) - offset 16 + float listenerUp[4]; // vec3 + padding (16 bytes) - offset 32 + float sourcePosition[4]; // vec3 + padding (16 bytes) - offset 48 + float sampleCount; // float (4 bytes) - offset 64 + float padding1[3]; // Padding to align to 16-byte boundary - offset 68 + uint32_t inputChannels; // uint (4 bytes) - offset 80 + uint32_t outputChannels; // uint (4 bytes) - offset 84 + uint32_t hrtfSize; // uint (4 bytes) - offset 88 + uint32_t numHrtfPositions; // uint (4 bytes) - offset 92 + float distanceAttenuation; // float (4 bytes) - offset 96 + float dopplerFactor; // float (4 bytes) - offset 100 + float reverbMix; // float (4 bytes) - offset 104 + float padding2; // Padding to complete 16-byte alignment - offset 108 + } params{}; + + // Copy listener and source positions with proper padding for GPU alignment + memcpy(params.listenerPosition, listenerPosition, sizeof(float) * 3); + params.listenerPosition[3] = 0.0f; // Padding for float3 alignment + memcpy(params.listenerForward, &listenerOrientation[0], sizeof(float) * 3); // Forward vector + params.listenerForward[3] = 0.0f; // Padding for float3 alignment + memcpy(params.listenerUp, &listenerOrientation[3], sizeof(float) * 3); // Up vector + params.listenerUp[3] = 0.0f; // Padding for float3 alignment + memcpy(params.sourcePosition, sourcePosition, sizeof(float) * 3); + params.sourcePosition[3] = 0.0f; // Padding for float3 alignment + params.sampleCount = static_cast(sampleCount); // Number of samples to process + params.padding1[0] = params.padding1[1] = params.padding1[2] = 0.0f; // Initialize padding + params.inputChannels = 1; // Mono input + params.outputChannels = 2; // Stereo output + params.hrtfSize = hrtfSize; + params.numHrtfPositions = numHrtfPositions; + params.distanceAttenuation = 1.0f; + params.dopplerFactor = 1.0f; + params.reverbMix = 0.0f; + params.padding2 = 0.0f; // Initialize padding + + // Copy parameters to parameter buffer using persistent memory mapping + if (persistentParamsMemory) + { + memcpy(persistentParamsMemory, ¶ms, sizeof(HRTFParams)); + } + else + { + std::cerr << "ERROR: Persistent memory not available for GPU processing!" << std::endl; + throw std::runtime_error("Persistent memory required for GPU processing"); + } + + // Use renderer's main compute pipeline instead of dedicated HRTF pipeline + uint32_t workGroupSize = 64; // Must match the numthreads in the shader + uint32_t groupCountX = (sampleCount + workGroupSize - 1) / workGroupSize; + + // Use renderer's main compute pipeline dispatch method + auto computeFence = renderer->DispatchCompute(groupCountX, 1, 1, + *this->inputBuffer, *this->outputBuffer, + *this->hrtfBuffer, *this->paramsBuffer); + + // Wait for compute shader to complete using fence-based synchronization + const vk::raii::Device &device = renderer->GetRaiiDevice(); + vk::Result result = device.waitForFences(*computeFence, VK_TRUE, UINT64_MAX); + if (result != vk::Result::eSuccess) + { + std::cerr << "Failed to wait for compute fence: " << vk::to_string(result) << std::endl; + throw std::runtime_error("Fence wait failed"); + } + + // Copy results from output buffer to the output array + void *outputData = outputBufferMemory.mapMemory(0, sampleCount * 2 * sizeof(float)); + + memcpy(outputBuffer, outputData, sampleCount * 2 * sizeof(float)); + outputBufferMemory.unmapMemory(); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "GPU HRTF processing failed: " << e.what() << std::endl; + std::cerr << "CPU fallback disabled - GPU path required" << std::endl; + throw; // Re-throw the exception to ensure failure without CPU fallback + } + } } -bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) { - // Smart buffer reuse: only recreate if sample count changed significantly or buffers don't exist - if (currentSampleCount == sampleCount && *inputBuffer && *outputBuffer && *hrtfBuffer && *paramsBuffer) { - return true; - } - - // Ensure all GPU operations complete before cleaning up existing buffers - if (renderer) { - const vk::raii::Device& device = renderer->GetRaiiDevice(); - device.waitIdle(); - } - - // Clean up existing buffers only if we need to recreate them - cleanupHRTFBuffers(); - - if (!renderer) { - std::cerr << "AudioSystem::createHRTFBuffers: Renderer is null" << std::endl; - return false; - } - - const vk::raii::Device& device = renderer->GetRaiiDevice(); - try { - // Create input buffer (mono audio) - vk::BufferCreateInfo inputBufferInfo; - inputBufferInfo.size = sampleCount * sizeof(float); - inputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; - inputBufferInfo.sharingMode = vk::SharingMode::eExclusive; - - inputBuffer = vk::raii::Buffer(device, inputBufferInfo); - - vk::MemoryRequirements inputMemRequirements = inputBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo inputAllocInfo; - inputAllocInfo.allocationSize = inputMemRequirements.size; - inputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( - inputMemRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - inputBufferMemory = vk::raii::DeviceMemory(device, inputAllocInfo); - inputBuffer.bindMemory(*inputBufferMemory, 0); - - // Create output buffer (stereo audio) - vk::BufferCreateInfo outputBufferInfo; - outputBufferInfo.size = sampleCount * 2 * sizeof(float); // Stereo (2 channels) - outputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; - outputBufferInfo.sharingMode = vk::SharingMode::eExclusive; - - outputBuffer = vk::raii::Buffer(device, outputBufferInfo); - - vk::MemoryRequirements outputMemRequirements = outputBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo outputAllocInfo; - outputAllocInfo.allocationSize = outputMemRequirements.size; - outputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( - outputMemRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - outputBufferMemory = vk::raii::DeviceMemory(device, outputAllocInfo); - outputBuffer.bindMemory(*outputBufferMemory, 0); - - // Create HRTF data buffer - vk::BufferCreateInfo hrtfBufferInfo; - hrtfBufferInfo.size = hrtfData.size() * sizeof(float); - hrtfBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; - hrtfBufferInfo.sharingMode = vk::SharingMode::eExclusive; - - hrtfBuffer = vk::raii::Buffer(device, hrtfBufferInfo); - - vk::MemoryRequirements hrtfMemRequirements = hrtfBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo hrtfAllocInfo; - hrtfAllocInfo.allocationSize = hrtfMemRequirements.size; - hrtfAllocInfo.memoryTypeIndex = renderer->FindMemoryType( - hrtfMemRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - hrtfBufferMemory = vk::raii::DeviceMemory(device, hrtfAllocInfo); - hrtfBuffer.bindMemory(*hrtfBufferMemory, 0); - - // Copy HRTF data to buffer - void* hrtfMappedMemory = hrtfBufferMemory.mapMemory(0, hrtfData.size() * sizeof(float)); - memcpy(hrtfMappedMemory, hrtfData.data(), hrtfData.size() * sizeof(float)); - hrtfBufferMemory.unmapMemory(); - - // Create parameters buffer - use the correct GPU structure size - // The GPU processing uses a larger aligned structure (112 bytes) not the header struct (64 bytes) - struct alignas(16) GPUHRTFParams { - float listenerPosition[4]; // vec3 + padding (16 bytes) - float listenerForward[4]; // vec3 + padding (16 bytes) - float listenerUp[4]; // vec3 + padding (16 bytes) - float sourcePosition[4]; // vec3 + padding (16 bytes) - float sampleCount; // float (4 bytes) - float padding1[3]; // Padding to align to 16-byte boundary - uint32_t inputChannels; // uint (4 bytes) - uint32_t outputChannels; // uint (4 bytes) - uint32_t hrtfSize; // uint (4 bytes) - uint32_t numHrtfPositions; // uint (4 bytes) - float distanceAttenuation; // float (4 bytes) - float dopplerFactor; // float (4 bytes) - float reverbMix; // float (4 bytes) - float padding2; // Padding to complete 16-byte alignment - }; - - vk::BufferCreateInfo paramsBufferInfo; - paramsBufferInfo.size = sizeof(GPUHRTFParams); // Use correct GPU structure size (112 bytes) - paramsBufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; - paramsBufferInfo.sharingMode = vk::SharingMode::eExclusive; - - paramsBuffer = vk::raii::Buffer(device, paramsBufferInfo); - - vk::MemoryRequirements paramsMemRequirements = paramsBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo paramsAllocInfo; - paramsAllocInfo.allocationSize = paramsMemRequirements.size; - paramsAllocInfo.memoryTypeIndex = renderer->FindMemoryType( - paramsMemRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - paramsBufferMemory = vk::raii::DeviceMemory(device, paramsAllocInfo); - paramsBuffer.bindMemory(*paramsBufferMemory, 0); - - // Set up persistent memory mapping for parameters buffer to avoid repeated map/unmap operations - persistentParamsMemory = paramsBufferMemory.mapMemory(0, sizeof(GPUHRTFParams)); - // Update current sample count to track buffer size - currentSampleCount = sampleCount; - return true; - } - catch (const std::exception& e) { - std::cerr << "Error creating HRTF buffers: " << e.what() << std::endl; - cleanupHRTFBuffers(); - return false; - } +bool AudioSystem::createHRTFBuffers(uint32_t sampleCount) +{ + // Smart buffer reuse: only recreate if sample count changed significantly or buffers don't exist + if (currentSampleCount == sampleCount && *inputBuffer && *outputBuffer && *hrtfBuffer && *paramsBuffer) + { + return true; + } + + // Ensure all GPU operations complete before cleaning up existing buffers. + // External synchronization required (VVL): use renderer helper which serializes against queue usage. + if (renderer) + { + renderer->WaitIdle(); + } + + // Clean up existing buffers only if we need to recreate them + cleanupHRTFBuffers(); + + if (!renderer) + { + std::cerr << "AudioSystem::createHRTFBuffers: Renderer is null" << std::endl; + return false; + } + + const vk::raii::Device &device = renderer->GetRaiiDevice(); + try + { + // Create input buffer (mono audio) + vk::BufferCreateInfo inputBufferInfo; + inputBufferInfo.size = sampleCount * sizeof(float); + inputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + inputBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + inputBuffer = vk::raii::Buffer(device, inputBufferInfo); + + vk::MemoryRequirements inputMemRequirements = inputBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo inputAllocInfo; + inputAllocInfo.allocationSize = inputMemRequirements.size; + inputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + inputMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + inputBufferMemory = vk::raii::DeviceMemory(device, inputAllocInfo); + inputBuffer.bindMemory(*inputBufferMemory, 0); + + // Create output buffer (stereo audio) + vk::BufferCreateInfo outputBufferInfo; + outputBufferInfo.size = sampleCount * 2 * sizeof(float); // Stereo (2 channels) + outputBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + outputBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + outputBuffer = vk::raii::Buffer(device, outputBufferInfo); + + vk::MemoryRequirements outputMemRequirements = outputBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo outputAllocInfo; + outputAllocInfo.allocationSize = outputMemRequirements.size; + outputAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + outputMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + outputBufferMemory = vk::raii::DeviceMemory(device, outputAllocInfo); + outputBuffer.bindMemory(*outputBufferMemory, 0); + + // Create HRTF data buffer + vk::BufferCreateInfo hrtfBufferInfo; + hrtfBufferInfo.size = hrtfData.size() * sizeof(float); + hrtfBufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + hrtfBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + hrtfBuffer = vk::raii::Buffer(device, hrtfBufferInfo); + + vk::MemoryRequirements hrtfMemRequirements = hrtfBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo hrtfAllocInfo; + hrtfAllocInfo.allocationSize = hrtfMemRequirements.size; + hrtfAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + hrtfMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + hrtfBufferMemory = vk::raii::DeviceMemory(device, hrtfAllocInfo); + hrtfBuffer.bindMemory(*hrtfBufferMemory, 0); + + // Copy HRTF data to buffer + void *hrtfMappedMemory = hrtfBufferMemory.mapMemory(0, hrtfData.size() * sizeof(float)); + memcpy(hrtfMappedMemory, hrtfData.data(), hrtfData.size() * sizeof(float)); + hrtfBufferMemory.unmapMemory(); + + // Create parameters buffer - use the correct GPU structure size + // The GPU processing uses a larger aligned structure (112 bytes) not the header struct (64 bytes) + struct alignas(16) GPUHRTFParams + { + float listenerPosition[4]; // vec3 + padding (16 bytes) + float listenerForward[4]; // vec3 + padding (16 bytes) + float listenerUp[4]; // vec3 + padding (16 bytes) + float sourcePosition[4]; // vec3 + padding (16 bytes) + float sampleCount; // float (4 bytes) + float padding1[3]; // Padding to align to 16-byte boundary + uint32_t inputChannels; // uint (4 bytes) + uint32_t outputChannels; // uint (4 bytes) + uint32_t hrtfSize; // uint (4 bytes) + uint32_t numHrtfPositions; // uint (4 bytes) + float distanceAttenuation; // float (4 bytes) + float dopplerFactor; // float (4 bytes) + float reverbMix; // float (4 bytes) + float padding2; // Padding to complete 16-byte alignment + }; + + vk::BufferCreateInfo paramsBufferInfo; + paramsBufferInfo.size = sizeof(GPUHRTFParams); // Use correct GPU structure size (112 bytes) + paramsBufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; + paramsBufferInfo.sharingMode = vk::SharingMode::eExclusive; + + paramsBuffer = vk::raii::Buffer(device, paramsBufferInfo); + + vk::MemoryRequirements paramsMemRequirements = paramsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo paramsAllocInfo; + paramsAllocInfo.allocationSize = paramsMemRequirements.size; + paramsAllocInfo.memoryTypeIndex = renderer->FindMemoryType( + paramsMemRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + paramsBufferMemory = vk::raii::DeviceMemory(device, paramsAllocInfo); + paramsBuffer.bindMemory(*paramsBufferMemory, 0); + + // Set up persistent memory mapping for parameters buffer to avoid repeated map/unmap operations + persistentParamsMemory = paramsBufferMemory.mapMemory(0, sizeof(GPUHRTFParams)); + // Update current sample count to track buffer size + currentSampleCount = sampleCount; + return true; + } + catch (const std::exception &e) + { + std::cerr << "Error creating HRTF buffers: " << e.what() << std::endl; + cleanupHRTFBuffers(); + return false; + } } -void AudioSystem::cleanupHRTFBuffers() { - // Unmap persistent memory if it exists - if (persistentParamsMemory && *paramsBufferMemory) { - paramsBufferMemory.unmapMemory(); - persistentParamsMemory = nullptr; - } - - // With RAII, we just need to set the resources to nullptr - // The destructors will handle the cleanup - inputBuffer = nullptr; - inputBufferMemory = nullptr; - outputBuffer = nullptr; - outputBufferMemory = nullptr; - hrtfBuffer = nullptr; - hrtfBufferMemory = nullptr; - paramsBuffer = nullptr; - paramsBufferMemory = nullptr; - - // Reset sample count tracking - currentSampleCount = 0; +void AudioSystem::cleanupHRTFBuffers() +{ + // Unmap persistent memory if it exists + if (persistentParamsMemory && *paramsBufferMemory) + { + paramsBufferMemory.unmapMemory(); + persistentParamsMemory = nullptr; + } + + // With RAII, we just need to set the resources to nullptr + // The destructors will handle the cleanup + inputBuffer = nullptr; + inputBufferMemory = nullptr; + outputBuffer = nullptr; + outputBufferMemory = nullptr; + hrtfBuffer = nullptr; + hrtfBufferMemory = nullptr; + paramsBuffer = nullptr; + paramsBufferMemory = nullptr; + + // Reset sample count tracking + currentSampleCount = 0; } - // Threading implementation methods -void AudioSystem::startAudioThread() { - if (audioThreadRunning.load()) { - return; // Thread already running - } +void AudioSystem::startAudioThread() +{ + if (audioThreadRunning.load()) + { + return; // Thread already running + } - audioThreadShouldStop.store(false); - audioThreadRunning.store(true); + audioThreadShouldStop.store(false); + audioThreadRunning.store(true); - audioThread = std::thread(&AudioSystem::audioThreadLoop, this); + audioThread = std::thread(&AudioSystem::audioThreadLoop, this); } -void AudioSystem::stopAudioThread() { - if (!audioThreadRunning.load()) { - return; // Thread not running - } +void AudioSystem::stopAudioThread() +{ + if (!audioThreadRunning.load()) + { + return; // Thread not running + } - // Signal the thread to stop - audioThreadShouldStop.store(true); + // Signal the thread to stop + audioThreadShouldStop.store(true); - // Wake up the thread if it's waiting - audioCondition.notify_all(); + // Wake up the thread if it's waiting + audioCondition.notify_all(); - // Wait for the thread to finish - if (audioThread.joinable()) { - audioThread.join(); - } + // Wait for the thread to finish + if (audioThread.joinable()) + { + audioThread.join(); + } - audioThreadRunning.store(false); + audioThreadRunning.store(false); } -void AudioSystem::audioThreadLoop() { - while (!audioThreadShouldStop.load()) { - std::shared_ptr task = nullptr; - - // Wait for a task or stop signal - { - std::unique_lock lock(taskQueueMutex); - audioCondition.wait(lock, [this] { - return !audioTaskQueue.empty() || audioThreadShouldStop.load(); - }); - - if (audioThreadShouldStop.load()) { - break; - } - - if (!audioTaskQueue.empty()) { - task = audioTaskQueue.front(); - audioTaskQueue.pop(); - } - } - - // Process the task if we have one - if (task) { - processAudioTask(task); - } - } +void AudioSystem::audioThreadLoop() +{ + while (!audioThreadShouldStop.load()) + { + std::shared_ptr task = nullptr; + + // Wait for a task or stop signal + { + std::unique_lock lock(taskQueueMutex); + audioCondition.wait(lock, [this] { + return !audioTaskQueue.empty() || audioThreadShouldStop.load(); + }); + + if (audioThreadShouldStop.load()) + { + break; + } + + if (!audioTaskQueue.empty()) + { + task = audioTaskQueue.front(); + audioTaskQueue.pop(); + } + } + + // Process the task if we have one + if (task) + { + processAudioTask(task); + } + } } -void AudioSystem::processAudioTask(const std::shared_ptr& task) { - // Process HRTF in the background thread - bool success = ProcessHRTF(task->inputBuffer.data(), task->outputBuffer.data(), - task->sampleCount, task->sourcePosition); - - if (success && task->outputDevice && task->outputDevice->IsPlaying()) { - // We used extended input of length sampleCount = histLen + outFrames. - // Trim the first trimFront frames from the stereo output and only write actualSamplesProcessed frames. - uint32_t startFrame = task->trimFront; - uint32_t framesToWrite = task->actualSamplesProcessed; - if (startFrame * 2 > task->outputBuffer.size()) { - startFrame = 0; // safety - } - if (startFrame * 2 + framesToWrite * 2 > task->outputBuffer.size()) { - framesToWrite = static_cast((task->outputBuffer.size() / 2) - startFrame); - } - float* startPtr = task->outputBuffer.data() + startFrame * 2; - // Apply master volume only to the range we will write - for (uint32_t i = 0; i < framesToWrite * 2; i++) { - startPtr[i] *= task->masterVolume; - } - // Send processed audio directly to output device from background thread - if (!task->outputDevice->WriteAudio(startPtr, framesToWrite)) { - std::cerr << "Failed to write audio data to output device from background thread" << std::endl; - } - } +void AudioSystem::processAudioTask(const std::shared_ptr &task) +{ + // Process HRTF in the background thread + bool success = ProcessHRTF(task->inputBuffer.data(), task->outputBuffer.data(), + task->sampleCount, task->sourcePosition); + + if (success && task->outputDevice && task->outputDevice->IsPlaying()) + { + // We used extended input of length sampleCount = histLen + outFrames. + // Trim the first trimFront frames from the stereo output and only write actualSamplesProcessed frames. + uint32_t startFrame = task->trimFront; + uint32_t framesToWrite = task->actualSamplesProcessed; + if (startFrame * 2 > task->outputBuffer.size()) + { + startFrame = 0; // safety + } + if (startFrame * 2 + framesToWrite * 2 > task->outputBuffer.size()) + { + framesToWrite = static_cast((task->outputBuffer.size() / 2) - startFrame); + } + float *startPtr = task->outputBuffer.data() + startFrame * 2; + // Apply master volume only to the range we will write + for (uint32_t i = 0; i < framesToWrite * 2; i++) + { + startPtr[i] *= task->masterVolume; + } + // Send processed audio directly to output device from background thread + if (!task->outputDevice->WriteAudio(startPtr, framesToWrite)) + { + std::cerr << "Failed to write audio data to output device from background thread" << std::endl; + } + } } -bool AudioSystem::submitAudioTask(const float* inputBuffer, uint32_t sampleCount, - const float* sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront) { - if (!audioThreadRunning.load()) { - // Fallback to synchronous processing if the thread is not running - std::vector outputBuffer(sampleCount * 2); - bool success = ProcessHRTF(inputBuffer, outputBuffer.data(), sampleCount, sourcePosition); - - if (success && outputDevice && outputDevice->IsPlaying()) { - // Apply master volume - for (uint32_t i = 0; i < sampleCount * 2; i++) { - outputBuffer[i] *= masterVolume; - } - - // Send to audio output device - if (!outputDevice->WriteAudio(outputBuffer.data(), sampleCount)) { - std::cerr << "Failed to write audio data to output device" << std::endl; - return false; - } - } - return success; - } - - // Create a new task for asynchronous processing - auto task = std::make_shared(); - task->inputBuffer.assign(inputBuffer, inputBuffer + sampleCount); - task->outputBuffer.resize(sampleCount * 2); // Stereo output - memcpy(task->sourcePosition, sourcePosition, sizeof(float) * 3); - task->sampleCount = sampleCount; // includes history frames - task->actualSamplesProcessed = actualSamplesProcessed; // new frames only (kChunk) - task->trimFront = sampleCount - actualSamplesProcessed; // history length (histLen) - task->outputDevice = outputDevice.get(); - task->masterVolume = masterVolume; - - // Submit the task to the queue (non-blocking) - { - std::lock_guard lock(taskQueueMutex); - audioTaskQueue.push(task); - } - audioCondition.notify_one(); - - return true; // Return immediately without waiting +bool AudioSystem::submitAudioTask(const float *inputBuffer, uint32_t sampleCount, + const float *sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront) +{ + if (!audioThreadRunning.load()) + { + // Fallback to synchronous processing if the thread is not running + std::vector outputBuffer(sampleCount * 2); + bool success = ProcessHRTF(inputBuffer, outputBuffer.data(), sampleCount, sourcePosition); + + if (success && outputDevice && outputDevice->IsPlaying()) + { + // Apply master volume + for (uint32_t i = 0; i < sampleCount * 2; i++) + { + outputBuffer[i] *= masterVolume; + } + + // Send to audio output device + if (!outputDevice->WriteAudio(outputBuffer.data(), sampleCount)) + { + std::cerr << "Failed to write audio data to output device" << std::endl; + return false; + } + } + return success; + } + + // Create a new task for asynchronous processing + auto task = std::make_shared(); + task->inputBuffer.assign(inputBuffer, inputBuffer + sampleCount); + task->outputBuffer.resize(sampleCount * 2); // Stereo output + memcpy(task->sourcePosition, sourcePosition, sizeof(float) * 3); + task->sampleCount = sampleCount; // includes history frames + task->actualSamplesProcessed = actualSamplesProcessed; // new frames only (kChunk) + task->trimFront = sampleCount - actualSamplesProcessed; // history length (histLen) + task->outputDevice = outputDevice.get(); + task->masterVolume = masterVolume; + + // Submit the task to the queue (non-blocking) + { + std::lock_guard lock(taskQueueMutex); + audioTaskQueue.push(task); + } + audioCondition.notify_one(); + + return true; // Return immediately without waiting } - - -void AudioSystem::FlushOutput() { - // Stop background processing to avoid races while flushing - stopAudioThread(); - - // Clear any pending audio processing tasks - { - std::lock_guard lock(taskQueueMutex); - std::queue> empty; - std::swap(audioTaskQueue, empty); - } - - // Flush the output device buffers and queues by restart - if (outputDevice) { - outputDevice->Stop(); - outputDevice->Start(); - } - - // Restart background processing - startAudioThread(); +void AudioSystem::FlushOutput() +{ + // Stop background processing to avoid races while flushing + stopAudioThread(); + + // Clear any pending audio processing tasks + { + std::lock_guard lock(taskQueueMutex); + std::queue> empty; + std::swap(audioTaskQueue, empty); + } + + // Flush the output device buffers and queues by restart + if (outputDevice) + { + outputDevice->Stop(); + outputDevice->Start(); + } + + // Restart background processing + startAudioThread(); } diff --git a/attachments/simple_engine/audio_system.h b/attachments/simple_engine/audio_system.h index b07a8ad6..d2ef04aa 100644 --- a/attachments/simple_engine/audio_system.h +++ b/attachments/simple_engine/audio_system.h @@ -1,81 +1,98 @@ +/* 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 /** * @brief Class representing an audio source. */ -class AudioSource { -public: - /** - * @brief Default constructor. - */ - AudioSource() = default; - - /** - * @brief Destructor for proper cleanup. - */ - virtual ~AudioSource() = default; - - /** - * @brief Play the audio source. - */ - virtual void Play() = 0; - - /** - * @brief Pause the audio source. - */ - virtual void Pause() = 0; - - /** - * @brief Stop the audio source. - */ - virtual void Stop() = 0; - - /** - * @brief Set the volume of the audio source. - * @param volume The volume (0.0f to 1.0f). - */ - virtual void SetVolume(float volume) = 0; - - /** - * @brief Set whether the audio source should loop. - * @param loop Whether to loop. - */ - virtual void SetLoop(bool loop) = 0; - - /** - * @brief Set the position of the audio source in 3D space. - * @param x The x-coordinate. - * @param y The y-coordinate. - * @param z The z-coordinate. - */ - virtual void SetPosition(float x, float y, float z) = 0; - - /** - * @brief Set the velocity of the audio source in 3D space. - * @param x The x-component. - * @param y The y-component. - * @param z The z-component. - */ - virtual void SetVelocity(float x, float y, float z) = 0; - - /** - * @brief Check if the audio source is playing. - * @return True if playing, false otherwise. - */ - virtual bool IsPlaying() const = 0; +class AudioSource +{ + public: + /** + * @brief Default constructor. + */ + AudioSource() = default; + + /** + * @brief Destructor for proper cleanup. + */ + virtual ~AudioSource() = default; + + /** + * @brief Play the audio source. + */ + virtual void Play() = 0; + + /** + * @brief Pause the audio source. + */ + virtual void Pause() = 0; + + /** + * @brief Stop the audio source. + */ + virtual void Stop() = 0; + + /** + * @brief Set the volume of the audio source. + * @param volume The volume (0.0f to 1.0f). + */ + virtual void SetVolume(float volume) = 0; + + /** + * @brief Set whether the audio source should loop. + * @param loop Whether to loop. + */ + virtual void SetLoop(bool loop) = 0; + + /** + * @brief Set the position of the audio source in 3D space. + * @param x The x-coordinate. + * @param y The y-coordinate. + * @param z The z-coordinate. + */ + virtual void SetPosition(float x, float y, float z) = 0; + + /** + * @brief Set the velocity of the audio source in 3D space. + * @param x The x-component. + * @param y The y-component. + * @param z The z-component. + */ + virtual void SetVelocity(float x, float y, float z) = 0; + + /** + * @brief Check if the audio source is playing. + * @return True if playing, false otherwise. + */ + virtual bool IsPlaying() const = 0; }; // Forward declarations @@ -85,326 +102,331 @@ class Engine; /** * @brief Interface for audio output devices. */ -class AudioOutputDevice { -public: - /** - * @brief Default constructor. - */ - AudioOutputDevice() = default; - - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~AudioOutputDevice() = default; - - /** - * @brief Initialize the audio output device. - * @param sampleRate The sample rate (e.g., 44100). - * @param channels The number of channels (typically 2 for stereo). - * @param bufferSize The buffer size in samples. - * @return True if initialization was successful, false otherwise. - */ - virtual bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) = 0; - - /** - * @brief Start audio playback. - * @return True if successful, false otherwise. - */ - virtual bool Start() = 0; - - /** - * @brief Stop audio playback. - * @return True if successful, false otherwise. - */ - virtual bool Stop() = 0; - - /** - * @brief Write audio data to the output device. - * @param data Pointer to the audio data (interleaved stereo float samples). - * @param sampleCount Number of samples per channel to write. - * @return True if successful, false otherwise. - */ - virtual bool WriteAudio(const float* data, uint32_t sampleCount) = 0; - - /** - * @brief Check if the device is currently playing. - * @return True if playing, false otherwise. - */ - virtual bool IsPlaying() const = 0; - - /** - * @brief Get the current playback position in samples. - * @return Current position in samples. - */ - virtual uint32_t GetPosition() const = 0; +class AudioOutputDevice +{ + public: + /** + * @brief Default constructor. + */ + AudioOutputDevice() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~AudioOutputDevice() = default; + + /** + * @brief Initialize the audio output device. + * @param sampleRate The sample rate (e.g., 44100). + * @param channels The number of channels (typically 2 for stereo). + * @param bufferSize The buffer size in samples. + * @return True if initialization was successful, false otherwise. + */ + virtual bool Initialize(uint32_t sampleRate, uint32_t channels, uint32_t bufferSize) = 0; + + /** + * @brief Start audio playback. + * @return True if successful, false otherwise. + */ + virtual bool Start() = 0; + + /** + * @brief Stop audio playback. + * @return True if successful, false otherwise. + */ + virtual bool Stop() = 0; + + /** + * @brief Write audio data to the output device. + * @param data Pointer to the audio data (interleaved stereo float samples). + * @param sampleCount Number of samples per channel to write. + * @return True if successful, false otherwise. + */ + virtual bool WriteAudio(const float *data, uint32_t sampleCount) = 0; + + /** + * @brief Check if the device is currently playing. + * @return True if playing, false otherwise. + */ + virtual bool IsPlaying() const = 0; + + /** + * @brief Get the current playback position in samples. + * @return Current position in samples. + */ + virtual uint32_t GetPosition() const = 0; }; /** * @brief Class for managing audio. */ -class AudioSystem { -public: - /** - * @brief Default constructor. - */ - AudioSystem() = default; - - // Constructor-based initialization to replace separate Initialize() calls - AudioSystem(Engine* engine, Renderer* renderer) { - if (!Initialize(engine, renderer)) { - throw std::runtime_error("AudioSystem: initialization failed"); - } - } - - /** - * @brief Flush audio output: clears pending processing and device buffers so playback restarts cleanly. - */ - void FlushOutput(); - - /** - * @brief Destructor for proper cleanup. - */ - ~AudioSystem(); - - /** - * @brief Initialize the audio system. - * @param engine Pointer to the engine for accessing active camera. - * @param renderer Pointer to the renderer for compute shader support. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(Engine* engine, Renderer* renderer = nullptr); - - /** - * @brief Update the audio system. - * @param deltaTime The time elapsed since the last update. - */ - void Update(std::chrono::milliseconds deltaTime); - - /** - * @brief Load an audio file. - * @param filename The path to the audio file. - * @param name The name to assign to the audio. - * @return True if loading was successful, false otherwise. - */ - bool LoadAudio(const std::string& filename, const std::string& name); - - /** - * @brief Create an audio source. - * @param name The name of the audio to use. - * @return Pointer to the created audio source, or nullptr if creation failed. - */ - AudioSource* CreateAudioSource(const std::string& name); - - /** - * @brief Create a sine wave ping audio source for debugging. - * @param name The name to assign to the debug audio source. - * @return Pointer to the created audio source, or nullptr if creation failed. - */ - AudioSource* CreateDebugPingSource(const std::string& name); - - /** - * @brief Set the listener position in 3D space. - * @param x The x-coordinate. - * @param y The y-coordinate. - * @param z The z-coordinate. - */ - void SetListenerPosition(float x, float y, float z); - - /** - * @brief Set the listener orientation in 3D space. - * @param forwardX The x-component of the forward vector. - * @param forwardY The y-component of the forward vector. - * @param forwardZ The z-component of the forward vector. - * @param upX The x-component of the up vector. - * @param upY The y-component of the up vector. - * @param upZ The z-component of the up vector. - */ - void SetListenerOrientation(float forwardX, float forwardY, float forwardZ, - float upX, float upY, float upZ); - - /** - * @brief Set the listener velocity in 3D space. - * @param x The x-component. - * @param y The y-component. - * @param z The z-component. - */ - void SetListenerVelocity(float x, float y, float z); - - /** - * @brief Set the master volume. - * @param volume The volume (0.0f to 1.0f). - */ - void SetMasterVolume(float volume); - - /** - * @brief Enable HRTF (Head-Related Transfer Function) processing. - * @param enable Whether to enable HRTF processing. - */ - void EnableHRTF(bool enable); - - /** - * @brief Check if HRTF processing is enabled. - * @return True if HRTF processing is enabled, false otherwise. - */ - bool IsHRTFEnabled() const; - - /** - * @brief Set whether to force CPU-only HRTF processing. - * @param cpuOnly Whether to force CPU-only processing (true) or allow Vulkan shader processing (false). - */ - void SetHRTFCPUOnly(bool cpuOnly); - - /** - * @brief Check if HRTF processing is set to CPU-only mode. - * @return True if CPU-only mode is enabled, false if Vulkan shader processing is allowed. - */ - bool IsHRTFCPUOnly() const; - - /** - * @brief Load HRTF data from a file. - * @param filename The path to the HRTF data file. - * @return True if loading was successful, false otherwise. - */ - bool LoadHRTFData(const std::string& filename); - - /** - * @brief Process audio data with HRTF. - * @param inputBuffer The input audio buffer. - * @param outputBuffer The output audio buffer. - * @param sampleCount The number of samples to process. - * @param sourcePosition The position of the sound source. - * @return True if processing was successful, false otherwise. - */ - bool ProcessHRTF(const float* inputBuffer, float* outputBuffer, uint32_t sampleCount, const float* sourcePosition); - - /** - * @brief Generate a sine wave ping for debugging purposes. - * @param buffer The output buffer to fill with ping audio data. - * @param sampleCount The number of samples to generate. - * @param playbackPosition The current playback position for timing. - */ - static void GenerateSineWavePing(float* buffer, uint32_t sampleCount, uint32_t playbackPosition); - -private: - // Loaded audio data - std::unordered_map> audioData; - - // Audio sources - std::vector> sources; - - // Listener properties - float listenerPosition[3] = {0.0f, 0.0f, 0.0f}; - float listenerOrientation[6] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; - float listenerVelocity[3] = {0.0f, 0.0f, 0.0f}; - - // Master volume - float masterVolume = 1.0f; - - // Whether the audio system is initialized - bool initialized = false; - - // HRTF processing - bool hrtfEnabled = false; - bool hrtfCPUOnly = false; - std::vector hrtfData; - uint32_t hrtfSize = 0; - uint32_t numHrtfPositions = 0; - - // Renderer for compute shader support - Renderer* renderer = nullptr; - - // Engine reference for accessing active camera - Engine* engine = nullptr; - - // Audio output device for sending processed audio to speakers - std::unique_ptr outputDevice = nullptr; - - // Threading infrastructure for background audio processing - std::thread audioThread; - std::mutex audioMutex; - std::condition_variable audioCondition; - std::atomic audioThreadRunning{false}; - std::atomic audioThreadShouldStop{false}; - - // Audio processing task queue - struct AudioTask { - std::vector inputBuffer; - std::vector outputBuffer; - float sourcePosition[3]; - uint32_t sampleCount; // total frames in input/output (may include history) - uint32_t actualSamplesProcessed; // frames to write this tick (new part) - uint32_t trimFront; // frames to skip from output front (history length) - AudioOutputDevice* outputDevice; - float masterVolume; - }; - // Set up HRTF parameters - struct HRTFParams { - float sourcePosition[3]; - float listenerPosition[3]; - float listenerOrientation[6]; // Forward (3) and up (3) vectors - uint32_t sampleCount; - uint32_t hrtfSize; - uint32_t numHrtfPositions; - float padding; // For alignment - } params; - std::queue> audioTaskQueue; - std::mutex taskQueueMutex; - - // Vulkan resources for HRTF processing - vk::raii::Buffer inputBuffer = nullptr; - vk::raii::DeviceMemory inputBufferMemory = nullptr; - vk::raii::Buffer outputBuffer = nullptr; - vk::raii::DeviceMemory outputBufferMemory = nullptr; - vk::raii::Buffer hrtfBuffer = nullptr; - vk::raii::DeviceMemory hrtfBufferMemory = nullptr; - vk::raii::Buffer paramsBuffer = nullptr; - vk::raii::DeviceMemory paramsBufferMemory = nullptr; - - // Persistent memory mapping for UBO to avoid repeated map/unmap operations - void* persistentParamsMemory = nullptr; - uint32_t currentSampleCount = 0; // Track current buffer size to avoid unnecessary recreation - - /** - * @brief Create buffers for HRTF processing. - * @param sampleCount The number of samples to process. - * @return True if creation was successful, false otherwise. - */ - bool createHRTFBuffers(uint32_t sampleCount); - - /** - * @brief Clean up HRTF buffers. - */ - void cleanupHRTFBuffers(); - - - /** - * @brief Start the background audio processing thread. - */ - void startAudioThread(); - - /** - * @brief Stop the background audio processing thread. - */ - void stopAudioThread(); - - /** - * @brief Main loop for the background audio processing thread. - */ - void audioThreadLoop(); - - /** - * @brief Process an audio task in the background thread. - * @param task The audio task to process. - */ - void processAudioTask(const std::shared_ptr& task); - - /** - * @brief Submit an audio processing task to the background thread. - * @param inputBuffer The input audio buffer. - * @param sampleCount The number of samples to process. - * @param sourcePosition The position of the sound source. - * @param actualSamplesProcessed The number of samples actually processed. - * @return True if the task was submitted successfully, false otherwise. - */ - bool submitAudioTask(const float* inputBuffer, uint32_t sampleCount, const float* sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront); +class AudioSystem +{ + public: + /** + * @brief Default constructor. + */ + AudioSystem() = default; + + // Constructor-based initialization to replace separate Initialize() calls + AudioSystem(Engine *engine, Renderer *renderer) + { + if (!Initialize(engine, renderer)) + { + throw std::runtime_error("AudioSystem: initialization failed"); + } + } + + /** + * @brief Flush audio output: clears pending processing and device buffers so playback restarts cleanly. + */ + void FlushOutput(); + + /** + * @brief Destructor for proper cleanup. + */ + ~AudioSystem(); + + /** + * @brief Initialize the audio system. + * @param engine Pointer to the engine for accessing active camera. + * @param renderer Pointer to the renderer for compute shader support. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Engine *engine, Renderer *renderer = nullptr); + + /** + * @brief Update the audio system. + * @param deltaTime The time elapsed since the last update. + */ + void Update(std::chrono::milliseconds deltaTime); + + /** + * @brief Load an audio file. + * @param filename The path to the audio file. + * @param name The name to assign to the audio. + * @return True if loading was successful, false otherwise. + */ + bool LoadAudio(const std::string &filename, const std::string &name); + + /** + * @brief Create an audio source. + * @param name The name of the audio to use. + * @return Pointer to the created audio source, or nullptr if creation failed. + */ + AudioSource *CreateAudioSource(const std::string &name); + + /** + * @brief Create a sine wave ping audio source for debugging. + * @param name The name to assign to the debug audio source. + * @return Pointer to the created audio source, or nullptr if creation failed. + */ + AudioSource *CreateDebugPingSource(const std::string &name); + + /** + * @brief Set the listener position in 3D space. + * @param x The x-coordinate. + * @param y The y-coordinate. + * @param z The z-coordinate. + */ + void SetListenerPosition(float x, float y, float z); + + /** + * @brief Set the listener orientation in 3D space. + * @param forwardX The x-component of the forward vector. + * @param forwardY The y-component of the forward vector. + * @param forwardZ The z-component of the forward vector. + * @param upX The x-component of the up vector. + * @param upY The y-component of the up vector. + * @param upZ The z-component of the up vector. + */ + void SetListenerOrientation(float forwardX, float forwardY, float forwardZ, + float upX, float upY, float upZ); + + /** + * @brief Set the listener velocity in 3D space. + * @param x The x-component. + * @param y The y-component. + * @param z The z-component. + */ + void SetListenerVelocity(float x, float y, float z); + + /** + * @brief Set the master volume. + * @param volume The volume (0.0f to 1.0f). + */ + void SetMasterVolume(float volume); + + /** + * @brief Enable HRTF (Head-Related Transfer Function) processing. + * @param enable Whether to enable HRTF processing. + */ + void EnableHRTF(bool enable); + + /** + * @brief Check if HRTF processing is enabled. + * @return True if HRTF processing is enabled, false otherwise. + */ + bool IsHRTFEnabled() const; + + /** + * @brief Set whether to force CPU-only HRTF processing. + * @param cpuOnly Whether to force CPU-only processing (true) or allow Vulkan shader processing (false). + */ + void SetHRTFCPUOnly(bool cpuOnly); + + /** + * @brief Check if HRTF processing is set to CPU-only mode. + * @return True if CPU-only mode is enabled, false if Vulkan shader processing is allowed. + */ + bool IsHRTFCPUOnly() const; + + /** + * @brief Load HRTF data from a file. + * @param filename The path to the HRTF data file. + * @return True if loading was successful, false otherwise. + */ + bool LoadHRTFData(const std::string &filename); + + /** + * @brief Process audio data with HRTF. + * @param inputBuffer The input audio buffer. + * @param outputBuffer The output audio buffer. + * @param sampleCount The number of samples to process. + * @param sourcePosition The position of the sound source. + * @return True if processing was successful, false otherwise. + */ + bool ProcessHRTF(const float *inputBuffer, float *outputBuffer, uint32_t sampleCount, const float *sourcePosition); + + /** + * @brief Generate a sine wave ping for debugging purposes. + * @param buffer The output buffer to fill with ping audio data. + * @param sampleCount The number of samples to generate. + * @param playbackPosition The current playback position for timing. + */ + static void GenerateSineWavePing(float *buffer, uint32_t sampleCount, uint32_t playbackPosition); + + private: + // Loaded audio data + std::unordered_map> audioData; + + // Audio sources + std::vector> sources; + + // Listener properties + float listenerPosition[3] = {0.0f, 0.0f, 0.0f}; + float listenerOrientation[6] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; + float listenerVelocity[3] = {0.0f, 0.0f, 0.0f}; + + // Master volume + float masterVolume = 1.0f; + + // Whether the audio system is initialized + bool initialized = false; + + // HRTF processing + bool hrtfEnabled = false; + bool hrtfCPUOnly = false; + std::vector hrtfData; + uint32_t hrtfSize = 0; + uint32_t numHrtfPositions = 0; + + // Renderer for compute shader support + Renderer *renderer = nullptr; + + // Engine reference for accessing active camera + Engine *engine = nullptr; + + // Audio output device for sending processed audio to speakers + std::unique_ptr outputDevice = nullptr; + + // Threading infrastructure for background audio processing + std::thread audioThread; + std::mutex audioMutex; + std::condition_variable audioCondition; + std::atomic audioThreadRunning{false}; + std::atomic audioThreadShouldStop{false}; + + // Audio processing task queue + struct AudioTask + { + std::vector inputBuffer; + std::vector outputBuffer; + float sourcePosition[3]; + uint32_t sampleCount; // total frames in input/output (may include history) + uint32_t actualSamplesProcessed; // frames to write this tick (new part) + uint32_t trimFront; // frames to skip from output front (history length) + AudioOutputDevice *outputDevice; + float masterVolume; + }; + // Set up HRTF parameters + struct HRTFParams + { + float sourcePosition[3]; + float listenerPosition[3]; + float listenerOrientation[6]; // Forward (3) and up (3) vectors + uint32_t sampleCount; + uint32_t hrtfSize; + uint32_t numHrtfPositions; + float padding; // For alignment + } params; + std::queue> audioTaskQueue; + std::mutex taskQueueMutex; + + // Vulkan resources for HRTF processing + vk::raii::Buffer inputBuffer = nullptr; + vk::raii::DeviceMemory inputBufferMemory = nullptr; + vk::raii::Buffer outputBuffer = nullptr; + vk::raii::DeviceMemory outputBufferMemory = nullptr; + vk::raii::Buffer hrtfBuffer = nullptr; + vk::raii::DeviceMemory hrtfBufferMemory = nullptr; + vk::raii::Buffer paramsBuffer = nullptr; + vk::raii::DeviceMemory paramsBufferMemory = nullptr; + + // Persistent memory mapping for UBO to avoid repeated map/unmap operations + void *persistentParamsMemory = nullptr; + uint32_t currentSampleCount = 0; // Track current buffer size to avoid unnecessary recreation + + /** + * @brief Create buffers for HRTF processing. + * @param sampleCount The number of samples to process. + * @return True if creation was successful, false otherwise. + */ + bool createHRTFBuffers(uint32_t sampleCount); + + /** + * @brief Clean up HRTF buffers. + */ + void cleanupHRTFBuffers(); + + /** + * @brief Start the background audio processing thread. + */ + void startAudioThread(); + + /** + * @brief Stop the background audio processing thread. + */ + void stopAudioThread(); + + /** + * @brief Main loop for the background audio processing thread. + */ + void audioThreadLoop(); + + /** + * @brief Process an audio task in the background thread. + * @param task The audio task to process. + */ + void processAudioTask(const std::shared_ptr &task); + + /** + * @brief Submit an audio processing task to the background thread. + * @param inputBuffer The input audio buffer. + * @param sampleCount The number of samples to process. + * @param sourcePosition The position of the sound source. + * @param actualSamplesProcessed The number of samples actually processed. + * @return True if the task was submitted successfully, false otherwise. + */ + bool submitAudioTask(const float *inputBuffer, uint32_t sampleCount, const float *sourcePosition, uint32_t actualSamplesProcessed, uint32_t trimFront); }; diff --git a/attachments/simple_engine/camera_component.cpp b/attachments/simple_engine/camera_component.cpp index 45f1b9f5..c6a2e77d 100644 --- a/attachments/simple_engine/camera_component.cpp +++ b/attachments/simple_engine/camera_component.cpp @@ -1,6 +1,23 @@ +/* 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 "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 @@ -10,51 +27,83 @@ // 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(); +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; +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; +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) { - glm::vec3 position = transformComponent->GetPosition(); - viewMatrix = glm::lookAt(position, target, up); - } else { - viewMatrix = glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), target, up); - } - viewMatrixDirty = false; +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; +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; } diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h index 93968de1..0d3225b0 100644 --- a/attachments/simple_engine/camera_component.h +++ b/attachments/simple_engine/camera_component.h @@ -1,11 +1,27 @@ +/* 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 "component.h" -#include "transform_component.h" #include "entity.h" +#include "transform_component.h" /** * @brief Component that handles the camera view and projection. @@ -13,207 +29,234 @@ * 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); - - // 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; - } - - /** - * @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; - } - -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(); +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); + + // 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; + } + + 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/simple_engine/component.cpp b/attachments/simple_engine/component.cpp index f2cb04ba..ba6eac03 100644 --- a/attachments/simple_engine/component.cpp +++ b/attachments/simple_engine/component.cpp @@ -1,3 +1,19 @@ +/* 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 "component.h" // Most of the Component class implementation is in the header file diff --git a/attachments/simple_engine/component.h b/attachments/simple_engine/component.h index 65480c71..8d0dcdd7 100644 --- a/attachments/simple_engine/component.h +++ b/attachments/simple_engine/component.h @@ -1,3 +1,19 @@ +/* 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 @@ -15,70 +31,91 @@ class Entity; * This class implements the component system as described in the Engine_Architecture chapter: * https://github.com/KhronosGroup/Vulkan-Tutorial/blob/master/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc */ -class Component { -protected: - Entity* owner = nullptr; - std::string name; - bool active = true; +class Component +{ + protected: + Entity *owner = nullptr; + std::string name; + bool active = true; -public: - /** - * @brief Constructor with optional name. - * @param componentName The name of the component. - */ - explicit Component(const std::string& componentName = "Component") : name(componentName) {} + public: + /** + * @brief Constructor with optional name. + * @param componentName The name of the component. + */ + explicit Component(const std::string &componentName = "Component") : + name(componentName) + {} - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~Component() = default; + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Component() = default; - /** - * @brief Initialize the component. - * Called when the component is added to an entity. - */ - virtual void Initialize() {} + /** + * @brief Initialize the component. + * Called when the component is added to an entity. + */ + virtual void Initialize() + {} - /** - * @brief Update the component. - * Called every frame. - * @param deltaTime The time elapsed since the last frame. - */ - virtual void Update(std::chrono::milliseconds deltaTime) {} + /** + * @brief Update the component. + * Called every frame. + * @param deltaTime The time elapsed since the last frame. + */ + virtual void Update(std::chrono::milliseconds deltaTime) + {} - /** - * @brief Render the component. - * Called during the rendering phase. - */ - virtual void Render() {} + /** + * @brief Render the component. + * Called during the rendering phase. + */ + virtual void Render() + {} - /** - * @brief Set the owner entity of this component. - * @param entity The entity that owns this component. - */ - void SetOwner(Entity* entity) { owner = entity; } + /** + * @brief Set the owner entity of this component. + * @param entity The entity that owns this component. + */ + void SetOwner(Entity *entity) + { + owner = entity; + } - /** - * @brief Get the owner entity of this component. - * @return The entity that owns this component. - */ - Entity* GetOwner() const { return owner; } + /** + * @brief Get the owner entity of this component. + * @return The entity that owns this component. + */ + Entity *GetOwner() const + { + return owner; + } - /** - * @brief Get the name of the component. - * @return The name of the component. - */ - const std::string& GetName() const { return name; } + /** + * @brief Get the name of the component. + * @return The name of the component. + */ + const std::string &GetName() const + { + return name; + } - /** - * @brief Check if the component is active. - * @return True if the component is active, false otherwise. - */ - bool IsActive() const { return active; } + /** + * @brief Check if the component is active. + * @return True if the component is active, false otherwise. + */ + bool IsActive() const + { + return active; + } - /** - * @brief Set the active state of the component. - * @param isActive The new active state. - */ - void SetActive(bool isActive) { active = isActive; } + /** + * @brief Set the active state of the component. + * @param isActive The new active state. + */ + void SetActive(bool isActive) + { + active = isActive; + } }; diff --git a/attachments/simple_engine/crash_reporter.h b/attachments/simple_engine/crash_reporter.h index 12c7ab4f..2a974f8d 100644 --- a/attachments/simple_engine/crash_reporter.h +++ b/attachments/simple_engine/crash_reporter.h @@ -1,23 +1,39 @@ +/* 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 #ifdef _WIN32 -#include -#include -#pragma comment(lib, "dbghelp.lib") +# include +# include +# pragma comment(lib, "dbghelp.lib") #elif defined(__APPLE__) || defined(__linux__) -#include -#include -#include +# include +# include +# include #endif #include "debug_system.h" @@ -28,260 +44,273 @@ * This class implements the crash reporting system as described in the Tooling chapter: * @see en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc */ -class CrashReporter { -public: - /** - * @brief Get the singleton instance of the crash reporter. - * @return Reference to the crash reporter instance. - */ - static CrashReporter& GetInstance() { - static CrashReporter instance; - return instance; - } - - /** - * @brief Initialize the crash reporter. - * @param minidumpDir The directory to store minidumps. - * @param appName The name of the application. - * @param appVersion The version of the application. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(const std::string& minidumpDir = "crashes", - const std::string& appName = "SimpleEngine", - const std::string& appVersion = "1.0.0") { - std::lock_guard lock(mutex); - - this->minidumpDir = minidumpDir; - this->appName = appName; - this->appVersion = appVersion; - - // Create minidump directory if it doesn't exist - #ifdef _WIN32 - CreateDirectoryA(minidumpDir.c_str(), NULL); - #else - std::string command = "mkdir -p " + minidumpDir; - system(command.c_str()); - #endif - - // Install crash handlers - InstallCrashHandlers(); - - // Register with debug system - DebugSystem::GetInstance().SetCrashHandler([this](const std::string& message) { - this->HandleCrash(message); - }); - - LOG_INFO("CrashReporter", "Crash reporter initialized"); - initialized = true; - return true; - } - - /** - * @brief Clean up crash reporter resources. - */ - void Cleanup() { - std::lock_guard lock(mutex); - - if (initialized) { - // Uninstall crash handlers - UninstallCrashHandlers(); - - LOG_INFO("CrashReporter", "Crash reporter shutting down"); - initialized = false; - } - } - - /** - * @brief Handle a crash. - * @param message The crash message. - */ - void HandleCrash(const std::string& message) { - std::lock_guard lock(mutex); - - LOG_FATAL("CrashReporter", "Crash detected: " + message); - - // Generate minidump - GenerateMinidump(message); - - // Call registered callbacks - for (const auto& callback : crashCallbacks) { - callback(message); - } - } - - /** - * @brief Register a crash callback. - * @param callback The callback function to be called when a crash occurs. - * @return An ID that can be used to unregister the callback. - */ - int RegisterCrashCallback(std::function callback) { - std::lock_guard lock(mutex); - - int id = nextCallbackId++; - crashCallbacks[id] = callback; - return id; - } - - /** - * @brief Unregister a crash callback. - * @param id The ID of the callback to unregister. - */ - void UnregisterCrashCallback(int id) { - std::lock_guard lock(mutex); - - crashCallbacks.erase(id); - } - - /** - * @brief Generate a minidump. - * @param message The crash message. - */ - void GenerateMinidump(const std::string& message) { - // Get current time for filename - auto now = std::chrono::system_clock::now(); - auto time = std::chrono::system_clock::to_time_t(now); - char timeStr[20]; - std::strftime(timeStr, sizeof(timeStr), "%Y%m%d_%H%M%S", std::localtime(&time)); - - // Create minidump filename - std::string filename = minidumpDir + "/" + appName + "_" + timeStr + ".dmp"; - - LOG_INFO("CrashReporter", "Generating minidump: " + filename); - - // Generate minidump based on platform - #ifdef _WIN32 - // Windows implementation - HANDLE hFile = CreateFileA( - filename.c_str(), - GENERIC_WRITE, - 0, - NULL, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - NULL - ); - - if (hFile != INVALID_HANDLE_VALUE) { - MINIDUMP_EXCEPTION_INFORMATION exInfo; - exInfo.ThreadId = GetCurrentThreadId(); - exInfo.ExceptionPointers = NULL; // Would be set in a real exception handler - exInfo.ClientPointers = FALSE; - - MiniDumpWriteDump( - GetCurrentProcess(), - GetCurrentProcessId(), - hFile, - MiniDumpNormal, - &exInfo, - NULL, - NULL - ); - - CloseHandle(hFile); - } - #else - // Unix implementation - std::ofstream file(filename, std::ios::out | std::ios::binary); - if (file.is_open()) { - // Get backtrace - void* callstack[128]; - int frames = backtrace(callstack, 128); - char** symbols = backtrace_symbols(callstack, frames); - - // Write header - file << "Crash Report for " << appName << " " << appVersion << std::endl; - file << "Timestamp: " << timeStr << std::endl; - file << "Message: " << message << std::endl; - file << std::endl; - - // Write backtrace - file << "Backtrace:" << std::endl; - for (int i = 0; i < frames; i++) { - file << symbols[i] << std::endl; - } - - free(symbols); - file.close(); - } - #endif - - LOG_INFO("CrashReporter", "Minidump generated: " + filename); - } - -private: - // Private constructor for singleton - CrashReporter() = default; - - // Delete copy constructor and assignment operator - CrashReporter(const CrashReporter&) = delete; - CrashReporter& operator=(const CrashReporter&) = delete; - - // Mutex for thread safety - std::mutex mutex; - - // Initialization flag - bool initialized = false; - - // Minidump directory - std::string minidumpDir = "crashes"; - - // Application info - std::string appName = "SimpleEngine"; - std::string appVersion = "1.0.0"; - - // Crash callbacks - std::unordered_map> crashCallbacks; - int nextCallbackId = 0; - - /** - * @brief Install platform-specific crash handlers. - */ - void InstallCrashHandlers() { - #ifdef _WIN32 - // Windows implementation - SetUnhandledExceptionFilter([](EXCEPTION_POINTERS* exInfo) -> LONG { - CrashReporter::GetInstance().HandleCrash("Unhandled exception"); - return EXCEPTION_EXECUTE_HANDLER; - }); - #else - // Unix implementation - signal(SIGSEGV, [](int sig) { - CrashReporter::GetInstance().HandleCrash("Segmentation fault"); - exit(1); - }); - - signal(SIGABRT, [](int sig) { - CrashReporter::GetInstance().HandleCrash("Abort"); - exit(1); - }); - - signal(SIGFPE, [](int sig) { - CrashReporter::GetInstance().HandleCrash("Floating point exception"); - exit(1); - }); - - signal(SIGILL, [](int sig) { - CrashReporter::GetInstance().HandleCrash("Illegal instruction"); - exit(1); - }); - #endif - } - - /** - * @brief Uninstall platform-specific crash handlers. - */ - void UninstallCrashHandlers() { - #ifdef _WIN32 - // Windows implementation - SetUnhandledExceptionFilter(NULL); - #else - // Unix implementation - signal(SIGSEGV, SIG_DFL); - signal(SIGABRT, SIG_DFL); - signal(SIGFPE, SIG_DFL); - signal(SIGILL, SIG_DFL); - #endif - } +class CrashReporter +{ + public: + /** + * @brief Get the singleton instance of the crash reporter. + * @return Reference to the crash reporter instance. + */ + static CrashReporter &GetInstance() + { + static CrashReporter instance; + return instance; + } + + /** + * @brief Initialize the crash reporter. + * @param minidumpDir The directory to store minidumps. + * @param appName The name of the application. + * @param appVersion The version of the application. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string &minidumpDir = "crashes", + const std::string &appName = "SimpleEngine", + const std::string &appVersion = "1.0.0") + { + std::lock_guard lock(mutex); + + this->minidumpDir = minidumpDir; + this->appName = appName; + this->appVersion = appVersion; + +// Create minidump directory if it doesn't exist +#ifdef _WIN32 + CreateDirectoryA(minidumpDir.c_str(), NULL); +#else + std::string command = "mkdir -p " + minidumpDir; + system(command.c_str()); +#endif + + // Install crash handlers + InstallCrashHandlers(); + + // Register with debug system + DebugSystem::GetInstance().SetCrashHandler([this](const std::string &message) { + this->HandleCrash(message); + }); + + LOG_INFO("CrashReporter", "Crash reporter initialized"); + initialized = true; + return true; + } + + /** + * @brief Clean up crash reporter resources. + */ + void Cleanup() + { + std::lock_guard lock(mutex); + + if (initialized) + { + // Uninstall crash handlers + UninstallCrashHandlers(); + + LOG_INFO("CrashReporter", "Crash reporter shutting down"); + initialized = false; + } + } + + /** + * @brief Handle a crash. + * @param message The crash message. + */ + void HandleCrash(const std::string &message) + { + std::lock_guard lock(mutex); + + LOG_FATAL("CrashReporter", "Crash detected: " + message); + + // Generate minidump + GenerateMinidump(message); + + // Call registered callbacks + for (const auto &callback : crashCallbacks) + { + callback(message); + } + } + + /** + * @brief Register a crash callback. + * @param callback The callback function to be called when a crash occurs. + * @return An ID that can be used to unregister the callback. + */ + int RegisterCrashCallback(std::function callback) + { + std::lock_guard lock(mutex); + + int id = nextCallbackId++; + crashCallbacks[id] = callback; + return id; + } + + /** + * @brief Unregister a crash callback. + * @param id The ID of the callback to unregister. + */ + void UnregisterCrashCallback(int id) + { + std::lock_guard lock(mutex); + + crashCallbacks.erase(id); + } + + /** + * @brief Generate a minidump. + * @param message The crash message. + */ + void GenerateMinidump(const std::string &message) + { + // Get current time for filename + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + char timeStr[20]; + std::strftime(timeStr, sizeof(timeStr), "%Y%m%d_%H%M%S", std::localtime(&time)); + + // Create minidump filename + std::string filename = minidumpDir + "/" + appName + "_" + timeStr + ".dmp"; + + LOG_INFO("CrashReporter", "Generating minidump: " + filename); + +// Generate minidump based on platform +#ifdef _WIN32 + // Windows implementation + HANDLE hFile = CreateFileA( + filename.c_str(), + GENERIC_WRITE, + 0, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + + if (hFile != INVALID_HANDLE_VALUE) + { + MINIDUMP_EXCEPTION_INFORMATION exInfo; + exInfo.ThreadId = GetCurrentThreadId(); + exInfo.ExceptionPointers = NULL; // Would be set in a real exception handler + exInfo.ClientPointers = FALSE; + + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + MiniDumpNormal, + &exInfo, + NULL, + NULL); + + CloseHandle(hFile); + } +#else + // Unix implementation + std::ofstream file(filename, std::ios::out | std::ios::binary); + if (file.is_open()) + { + // Get backtrace + void *callstack[128]; + int frames = backtrace(callstack, 128); + char **symbols = backtrace_symbols(callstack, frames); + + // Write header + file << "Crash Report for " << appName << " " << appVersion << std::endl; + file << "Timestamp: " << timeStr << std::endl; + file << "Message: " << message << std::endl; + file << std::endl; + + // Write backtrace + file << "Backtrace:" << std::endl; + for (int i = 0; i < frames; i++) + { + file << symbols[i] << std::endl; + } + + free(symbols); + file.close(); + } +#endif + + LOG_INFO("CrashReporter", "Minidump generated: " + filename); + } + + private: + // Private constructor for singleton + CrashReporter() = default; + + // Delete copy constructor and assignment operator + CrashReporter(const CrashReporter &) = delete; + CrashReporter &operator=(const CrashReporter &) = delete; + + // Mutex for thread safety + std::mutex mutex; + + // Initialization flag + bool initialized = false; + + // Minidump directory + std::string minidumpDir = "crashes"; + + // Application info + std::string appName = "SimpleEngine"; + std::string appVersion = "1.0.0"; + + // Crash callbacks + std::unordered_map> crashCallbacks; + int nextCallbackId = 0; + + /** + * @brief Install platform-specific crash handlers. + */ + void InstallCrashHandlers() + { +#ifdef _WIN32 + // Windows implementation + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS *exInfo) -> LONG { + CrashReporter::GetInstance().HandleCrash("Unhandled exception"); + return EXCEPTION_EXECUTE_HANDLER; + }); +#else + // Unix implementation + signal(SIGSEGV, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Segmentation fault"); + exit(1); + }); + + signal(SIGABRT, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Abort"); + exit(1); + }); + + signal(SIGFPE, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Floating point exception"); + exit(1); + }); + + signal(SIGILL, [](int sig) { + CrashReporter::GetInstance().HandleCrash("Illegal instruction"); + exit(1); + }); +#endif + } + + /** + * @brief Uninstall platform-specific crash handlers. + */ + void UninstallCrashHandlers() + { +#ifdef _WIN32 + // Windows implementation + SetUnhandledExceptionFilter(NULL); +#else + // Unix implementation + signal(SIGSEGV, SIG_DFL); + signal(SIGABRT, SIG_DFL); + signal(SIGFPE, SIG_DFL); + signal(SIGILL, SIG_DFL); +#endif + } }; // Convenience macro for simulating a crash (for testing) diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h index d1c81b24..f58748c7 100644 --- a/attachments/simple_engine/debug_system.h +++ b/attachments/simple_engine/debug_system.h @@ -1,24 +1,41 @@ +/* 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 /** * @brief Enum for different log levels. */ -enum class LogLevel { - Debug, - Info, - Warning, - Error, - Fatal +enum class LogLevel +{ + Debug, + Info, + Warning, + Error, + Fatal }; /** @@ -27,217 +44,238 @@ enum class LogLevel { * This class implements the debugging system as described in the Tooling chapter: * @see en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc */ -class DebugSystem { -public: - /** - * @brief Get the singleton instance of the debug system. - * @return Reference to the debug system instance. - */ - static DebugSystem& GetInstance() { - static DebugSystem instance; - return instance; - } - - /** - * @brief Initialize the debug system. - * @param logFilePath The path to the log file. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(const std::string& logFilePath = "engine.log") { - std::lock_guard lock(mutex); - - // Open log file - logFile.open(logFilePath, std::ios::out | std::ios::trunc); - if (!logFile.is_open()) { - std::cerr << "Failed to open log file: " << logFilePath << std::endl; - return false; - } - - // Log initialization - Log(LogLevel::Info, "DebugSystem", "Debug system initialized"); - - initialized = true; - return true; - } - - /** - * @brief Clean up debug system resources. - */ - void Cleanup() { - std::lock_guard lock(mutex); - - if (initialized) { - // Log cleanup - Log(LogLevel::Info, "DebugSystem", "Debug system shutting down"); - - // Close log file - if (logFile.is_open()) { - logFile.close(); - } - - initialized = false; - } - } - - /** - * @brief Log a message. - * @param level The log level. - * @param tag The tag for the log message. - * @param message The log message. - */ - void Log(LogLevel level, const std::string& tag, const std::string& message) { - std::lock_guard lock(mutex); - - // Get current time - auto now = std::chrono::system_clock::now(); - auto time = std::chrono::system_clock::to_time_t(now); - auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; - - char timeStr[20]; - std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", std::localtime(&time)); - - // Format log message - std::string levelStr; - switch (level) { - case LogLevel::Debug: - levelStr = "DEBUG"; - break; - case LogLevel::Info: - levelStr = "INFO"; - break; - case LogLevel::Warning: - levelStr = "WARNING"; - break; - case LogLevel::Error: - levelStr = "ERROR"; - break; - case LogLevel::Fatal: - levelStr = "FATAL"; - break; - } - - std::string formattedMessage = - std::string(timeStr) + "." + std::to_string(ms.count()) + - " [" + levelStr + "] " + - "[" + tag + "] " + - message; - - // Write to console - if (level >= LogLevel::Warning) { - std::cerr << formattedMessage << std::endl; - } else { - std::cout << formattedMessage << std::endl; - } - - // Write to log file - if (logFile.is_open()) { - logFile << formattedMessage << std::endl; - logFile.flush(); - } - - // Call registered callbacks - for (const auto& kv : logCallbacks) { - kv.second(level, tag, message); - } - - // If fatal, trigger crash handler - if (level == LogLevel::Fatal && crashHandler) { - crashHandler(formattedMessage); - } - } - - /** - * @brief Register a log callback. - * @param callback The callback function to be called when a log message is generated. - * @return An ID that can be used to unregister the callback. - */ - int RegisterLogCallback(std::function callback) { - std::lock_guard lock(mutex); - - int id = nextCallbackId++; - logCallbacks[id] = callback; - return id; - } - - /** - * @brief Unregister a log callback. - * @param id The ID of the callback to unregister. - */ - void UnregisterLogCallback(int id) { - std::lock_guard lock(mutex); - - logCallbacks.erase(id); - } - - /** - * @brief Set the crash handler. - * @param handler The crash handler function. - */ - void SetCrashHandler(std::function handler) { - std::lock_guard lock(mutex); - - crashHandler = handler; - } - - /** - * @brief Start a performance measurement. - * @param name The name of the measurement. - */ - void StartMeasurement(const std::string& name) { - std::lock_guard lock(mutex); - - auto now = std::chrono::high_resolution_clock::now(); - measurements[name] = now; - } - - /** - * @brief End a performance measurement and log the result. - * @param name The name of the measurement. - */ - void StopMeasurement(const std::string& name) { - auto now = std::chrono::high_resolution_clock::now(); - std::lock_guard lock(mutex); - - auto it = measurements.find(name); - - if (it != measurements.end()) { - auto duration = std::chrono::duration_cast(now - it->second).count(); - Log(LogLevel::Debug, "Performance", name + ": " + std::to_string(duration) + " us"); - measurements.erase(it); - } else { - Log(LogLevel::Error, "Performance", "No measurement started with name: " + name); - } - } - - -protected: - // Protected constructor for inheritance - DebugSystem() = default; - virtual ~DebugSystem() = default; - - // Delete copy constructor and assignment operator - DebugSystem(const DebugSystem&) = delete; - DebugSystem& operator=(const DebugSystem&) = delete; - - // Mutex for thread safety - std::mutex mutex; - - // Log file - std::ofstream logFile; - - // Initialization flag - bool initialized = false; - - // Log callbacks - std::unordered_map> logCallbacks; - int nextCallbackId = 0; - - // Crash handler - std::function crashHandler; - - // Performance measurements - std::unordered_map measurements; - +class DebugSystem +{ + public: + /** + * @brief Get the singleton instance of the debug system. + * @return Reference to the debug system instance. + */ + static DebugSystem &GetInstance() + { + static DebugSystem instance; + return instance; + } + + /** + * @brief Initialize the debug system. + * @param logFilePath The path to the log file. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string &logFilePath = "engine.log") + { + std::lock_guard lock(mutex); + + // Open log file + logFile.open(logFilePath, std::ios::out | std::ios::trunc); + if (!logFile.is_open()) + { + std::cerr << "Failed to open log file: " << logFilePath << std::endl; + return false; + } + + // Log initialization + Log(LogLevel::Info, "DebugSystem", "Debug system initialized"); + + initialized = true; + return true; + } + + /** + * @brief Clean up debug system resources. + */ + void Cleanup() + { + std::lock_guard lock(mutex); + + if (initialized) + { + // Log cleanup + Log(LogLevel::Info, "DebugSystem", "Debug system shutting down"); + + // Close log file + if (logFile.is_open()) + { + logFile.close(); + } + + initialized = false; + } + } + + /** + * @brief Log a message. + * @param level The log level. + * @param tag The tag for the log message. + * @param message The log message. + */ + void Log(LogLevel level, const std::string &tag, const std::string &message) + { + std::lock_guard lock(mutex); + + // Get current time + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + + char timeStr[20]; + std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", std::localtime(&time)); + + // Format log message + std::string levelStr; + switch (level) + { + case LogLevel::Debug: + levelStr = "DEBUG"; + break; + case LogLevel::Info: + levelStr = "INFO"; + break; + case LogLevel::Warning: + levelStr = "WARNING"; + break; + case LogLevel::Error: + levelStr = "ERROR"; + break; + case LogLevel::Fatal: + levelStr = "FATAL"; + break; + } + + std::string formattedMessage = + std::string(timeStr) + "." + std::to_string(ms.count()) + + " [" + levelStr + "] " + + "[" + tag + "] " + + message; + + // Write to console + if (level >= LogLevel::Warning) + { + std::cerr << formattedMessage << std::endl; + } + else + { + std::cout << formattedMessage << std::endl; + } + + // Write to log file + if (logFile.is_open()) + { + logFile << formattedMessage << std::endl; + logFile.flush(); + } + + // Call registered callbacks + for (const auto &kv : logCallbacks) + { + kv.second(level, tag, message); + } + + // If fatal, trigger crash handler + if (level == LogLevel::Fatal && crashHandler) + { + crashHandler(formattedMessage); + } + } + + /** + * @brief Register a log callback. + * @param callback The callback function to be called when a log message is generated. + * @return An ID that can be used to unregister the callback. + */ + int RegisterLogCallback(std::function callback) + { + std::lock_guard lock(mutex); + + int id = nextCallbackId++; + logCallbacks[id] = callback; + return id; + } + + /** + * @brief Unregister a log callback. + * @param id The ID of the callback to unregister. + */ + void UnregisterLogCallback(int id) + { + std::lock_guard lock(mutex); + + logCallbacks.erase(id); + } + + /** + * @brief Set the crash handler. + * @param handler The crash handler function. + */ + void SetCrashHandler(std::function handler) + { + std::lock_guard lock(mutex); + + crashHandler = handler; + } + + /** + * @brief Start a performance measurement. + * @param name The name of the measurement. + */ + void StartMeasurement(const std::string &name) + { + std::lock_guard lock(mutex); + + auto now = std::chrono::high_resolution_clock::now(); + measurements[name] = now; + } + + /** + * @brief End a performance measurement and log the result. + * @param name The name of the measurement. + */ + void StopMeasurement(const std::string &name) + { + auto now = std::chrono::high_resolution_clock::now(); + std::lock_guard lock(mutex); + + auto it = measurements.find(name); + + if (it != measurements.end()) + { + auto duration = std::chrono::duration_cast(now - it->second).count(); + Log(LogLevel::Debug, "Performance", name + ": " + std::to_string(duration) + " us"); + measurements.erase(it); + } + else + { + Log(LogLevel::Error, "Performance", "No measurement started with name: " + name); + } + } + + protected: + // Protected constructor for inheritance + DebugSystem() = default; + virtual ~DebugSystem() = default; + + // Delete copy constructor and assignment operator + DebugSystem(const DebugSystem &) = delete; + DebugSystem &operator=(const DebugSystem &) = delete; + + // Mutex for thread safety + std::mutex mutex; + + // Log file + std::ofstream logFile; + + // Initialization flag + bool initialized = false; + + // Log callbacks + std::unordered_map> logCallbacks; + int nextCallbackId = 0; + + // Crash handler + std::function crashHandler; + + // Performance measurements + std::unordered_map measurements; }; // Convenience macros for logging diff --git a/attachments/simple_engine/descriptor_manager.cpp b/attachments/simple_engine/descriptor_manager.cpp index e75f7830..494abd8d 100644 --- a/attachments/simple_engine/descriptor_manager.cpp +++ b/attachments/simple_engine/descriptor_manager.cpp @@ -1,220 +1,248 @@ +/* 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 "descriptor_manager.h" -#include +#include "camera_component.h" +#include "transform_component.h" #include #include -#include "transform_component.h" -#include "camera_component.h" +#include // Constructor -DescriptorManager::DescriptorManager(VulkanDevice& device) - : device(device) { +DescriptorManager::DescriptorManager(VulkanDevice &device) : + device(device) +{ } // Destructor DescriptorManager::~DescriptorManager() = default; // Create descriptor pool -bool DescriptorManager::createDescriptorPool(uint32_t maxSets) { - try { - // Create descriptor pool sizes - std::array poolSizes = { - vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eUniformBuffer, - .descriptorCount = maxSets - }, - vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eCombinedImageSampler, - .descriptorCount = maxSets - } - }; - - // Create descriptor pool - vk::DescriptorPoolCreateInfo poolInfo{ - .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = maxSets, - .poolSizeCount = static_cast(poolSizes.size()), - .pPoolSizes = poolSizes.data() - }; - - descriptorPool = vk::raii::DescriptorPool(device.getDevice(), poolInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; - return false; - } +bool DescriptorManager::createDescriptorPool(uint32_t maxSets) +{ + try + { + // Create descriptor pool sizes + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = maxSets}, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = maxSets}}; + + // Create descriptor pool + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = maxSets, + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data()}; + + descriptorPool = vk::raii::DescriptorPool(device.getDevice(), poolInfo); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } } // Create uniform buffers for an entity -bool DescriptorManager::createUniformBuffers(Entity* entity, uint32_t maxFramesInFlight) { - try { - // Create uniform buffers for each frame in flight - vk::DeviceSize bufferSize = sizeof(UniformBufferObject); - - // Create entity resources if they don't exist - auto entityResourcesIt = entityResources.try_emplace( entity ).first; - - // Clear existing uniform buffers - entityResourcesIt->second.uniformBuffers.clear(); - entityResourcesIt->second.uniformBuffersMemory.clear(); - entityResourcesIt->second.uniformBuffersMapped.clear(); - - // Create uniform buffers - for (size_t i = 0; i < maxFramesInFlight; i++) { - // Create buffer - auto [buffer, bufferMemory] = createBuffer( - bufferSize, - vk::BufferUsageFlagBits::eUniformBuffer, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - // Map memory - void* data = bufferMemory.mapMemory(0, bufferSize); - - // Store resources - entityResourcesIt->second.uniformBuffers.push_back(std::move(buffer)); - entityResourcesIt->second.uniformBuffersMemory.push_back(std::move(bufferMemory)); - entityResourcesIt->second.uniformBuffersMapped.push_back(data); - } - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create uniform buffers: " << e.what() << std::endl; - return false; - } +bool DescriptorManager::createUniformBuffers(Entity *entity, uint32_t maxFramesInFlight) +{ + try + { + // Create uniform buffers for each frame in flight + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + // Create entity resources if they don't exist + auto entityResourcesIt = entityResources.try_emplace(entity).first; + + // Clear existing uniform buffers + entityResourcesIt->second.uniformBuffers.clear(); + entityResourcesIt->second.uniformBuffersMemory.clear(); + entityResourcesIt->second.uniformBuffersMapped.clear(); + + // Create uniform buffers + for (size_t i = 0; i < maxFramesInFlight; i++) + { + // Create buffer + auto [buffer, bufferMemory] = createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Map memory + void *data = bufferMemory.mapMemory(0, bufferSize); + + // Store resources + entityResourcesIt->second.uniformBuffers.push_back(std::move(buffer)); + entityResourcesIt->second.uniformBuffersMemory.push_back(std::move(bufferMemory)); + entityResourcesIt->second.uniformBuffersMapped.push_back(data); + } + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create uniform buffers: " << e.what() << std::endl; + return false; + } } -bool DescriptorManager::update_descriptor_sets(Entity* entity, uint32_t maxFramesInFlight, bool& value1) { - assert(entityResources[entity].uniformBuffers.size() == maxFramesInFlight); - // Update descriptor sets - for (size_t i = 0; i < maxFramesInFlight; i++) { - // Create descriptor buffer info - vk::DescriptorBufferInfo bufferInfo{ - .buffer = *entityResources[entity].uniformBuffers[i], - .offset = 0, - .range = sizeof(UniformBufferObject) - }; - - // Create descriptor image info - vk::DescriptorImageInfo imageInfo{ - // These would be set based on the texture resources - // .sampler = textureSampler, - // .imageView = textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal - }; - - // Create descriptor writes - std::array descriptorWrites = { - vk::WriteDescriptorSet{ - .dstSet = entityResources[entity].descriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pImageInfo = nullptr, - .pBufferInfo = &bufferInfo, - .pTexelBufferView = nullptr - }, - vk::WriteDescriptorSet{ - .dstSet = entityResources[entity].descriptorSets[i], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfo, - .pBufferInfo = nullptr, - .pTexelBufferView = nullptr - } - }; - - // Update descriptor sets - device.getDevice().updateDescriptorSets(descriptorWrites, nullptr); - } - return false; +bool DescriptorManager::update_descriptor_sets(Entity *entity, uint32_t maxFramesInFlight, bool &value1) +{ + assert(entityResources[entity].uniformBuffers.size() == maxFramesInFlight); + // Update descriptor sets + for (size_t i = 0; i < maxFramesInFlight; i++) + { + // Create descriptor buffer info + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *entityResources[entity].uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject)}; + + // Create descriptor image info + vk::DescriptorImageInfo imageInfo{ + // These would be set based on the texture resources + // .sampler = textureSampler, + // .imageView = textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + + // Create descriptor writes + std::array descriptorWrites = { + vk::WriteDescriptorSet{ + .dstSet = entityResources[entity].descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pImageInfo = nullptr, + .pBufferInfo = &bufferInfo, + .pTexelBufferView = nullptr}, + vk::WriteDescriptorSet{ + .dstSet = entityResources[entity].descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo, + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr}}; + + // Update descriptor sets + device.getDevice().updateDescriptorSets(descriptorWrites, nullptr); + } + return false; } // Create descriptor sets for an entity -bool DescriptorManager::createDescriptorSets(Entity* entity, const std::string& texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight) { - try { - assert(entityResources.find(entity) != entityResources.end()); - // Create descriptor sets for each frame in flight - std::vector layouts(maxFramesInFlight, descriptorSetLayout); - - // Allocate descriptor sets - vk::DescriptorSetAllocateInfo allocInfo{ - .descriptorPool = *descriptorPool, - .descriptorSetCount = static_cast(maxFramesInFlight), - .pSetLayouts = layouts.data() - }; - - entityResources[entity].descriptorSets = device.getDevice().allocateDescriptorSets(allocInfo); - - bool value1; - if (update_descriptor_sets(entity, maxFramesInFlight, value1)) return value1; - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; - return false; - } +bool DescriptorManager::createDescriptorSets(Entity *entity, const std::string &texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight) +{ + try + { + assert(entityResources.find(entity) != entityResources.end()); + // Create descriptor sets for each frame in flight + std::vector layouts(maxFramesInFlight, descriptorSetLayout); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(maxFramesInFlight), + .pSetLayouts = layouts.data()}; + + entityResources[entity].descriptorSets = device.getDevice().allocateDescriptorSets(allocInfo); + + bool value1; + if (update_descriptor_sets(entity, maxFramesInFlight, value1)) + return value1; + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create descriptor sets: " << e.what() << std::endl; + return false; + } } // Update uniform buffer for an entity -void DescriptorManager::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { - // Update uniform buffer with the latest data - UniformBufferObject ubo{}; - - // Get entity transform - auto transform = entity->GetComponent(); - if (transform) { - ubo.model = transform->GetModelMatrix(); - } else { - ubo.model = glm::mat4(1.0f); - } - - // Get camera view and projection - if (camera) { - ubo.view = camera->GetViewMatrix(); - ubo.proj = camera->GetProjectionMatrix(); - ubo.viewPos = glm::vec4(camera->GetPosition(), 1.0f); - } else { - ubo.view = glm::mat4(1.0f); - ubo.proj = glm::mat4(1.0f); - ubo.viewPos = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - } - - // Set light position and color - ubo.lightPos = glm::vec4(0.0f, 5.0f, 0.0f, 1.0f); - ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - - assert(entityResources.find(entity) != entityResources.end()); - assert(entityResources[entity].uniformBuffers.size() > currentImage); - // Copy data to uniform buffer - memcpy(entityResources[entity].uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +void DescriptorManager::updateUniformBuffer(uint32_t currentImage, Entity *entity, CameraComponent *camera) +{ + // Update uniform buffer with the latest data + UniformBufferObject ubo{}; + + // Get entity transform + auto transform = entity->GetComponent(); + if (transform) + { + ubo.model = transform->GetModelMatrix(); + } + else + { + ubo.model = glm::mat4(1.0f); + } + + // Get camera view and projection + if (camera) + { + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + ubo.viewPos = glm::vec4(camera->GetPosition(), 1.0f); + } + else + { + ubo.view = glm::mat4(1.0f); + ubo.proj = glm::mat4(1.0f); + ubo.viewPos = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); + } + + // Set light position and color + ubo.lightPos = glm::vec4(0.0f, 5.0f, 0.0f, 1.0f); + ubo.lightColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); + + assert(entityResources.find(entity) != entityResources.end()); + assert(entityResources[entity].uniformBuffers.size() > currentImage); + // Copy data to uniform buffer + memcpy(entityResources[entity].uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); } // Create buffer -std::pair DescriptorManager::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties) { - // Create buffer - vk::BufferCreateInfo bufferInfo{ - .size = size, - .usage = usage, - .sharingMode = vk::SharingMode::eExclusive - }; +std::pair DescriptorManager::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties) +{ + // Create buffer + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive}; - vk::raii::Buffer buffer(device.getDevice(), bufferInfo); + vk::raii::Buffer buffer(device.getDevice(), bufferInfo); - // Allocate memory - vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + // Allocate memory + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); - vk::MemoryAllocateInfo allocInfo{ - .allocationSize = memRequirements.size, - .memoryTypeIndex = device.findMemoryType(memRequirements.memoryTypeBits, properties) - }; + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = device.findMemoryType(memRequirements.memoryTypeBits, properties)}; - vk::raii::DeviceMemory bufferMemory(device.getDevice(), allocInfo); + vk::raii::DeviceMemory bufferMemory(device.getDevice(), allocInfo); - // Bind memory - buffer.bindMemory(*bufferMemory, 0); + // Bind memory + buffer.bindMemory(*bufferMemory, 0); - return {std::move(buffer), std::move(bufferMemory)}; + return {std::move(buffer), std::move(bufferMemory)}; } diff --git a/attachments/simple_engine/descriptor_manager.h b/attachments/simple_engine/descriptor_manager.h index 48319b4d..77a46f46 100644 --- a/attachments/simple_engine/descriptor_manager.h +++ b/attachments/simple_engine/descriptor_manager.h @@ -1,25 +1,42 @@ +/* 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 #define GLM_FORCE_RADIANS #include #include -#include "vulkan_device.h" #include "entity.h" +#include "vulkan_device.h" /** * @brief Structure for uniform buffer object. */ -struct UniformBufferObject { - alignas(16) glm::mat4 model; - alignas(16) glm::mat4 view; - alignas(16) glm::mat4 proj; - alignas(16) glm::vec4 lightPos; - alignas(16) glm::vec4 lightColor; - alignas(16) glm::vec4 viewPos; +struct UniformBufferObject +{ + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + alignas(16) glm::vec4 lightPos; + alignas(16) glm::vec4 lightColor; + alignas(16) glm::vec4 viewPos; }; class CameraComponent; @@ -27,88 +44,99 @@ class CameraComponent; /** * @brief Class for managing Vulkan descriptor sets and layouts. */ -class DescriptorManager { -public: - // Entity resources - struct EntityResources { - std::vector uniformBuffers; - std::vector uniformBuffersMemory; - std::vector uniformBuffersMapped; - std::vector descriptorSets; - }; - - /** - * @brief Constructor. - * @param device The Vulkan device. - */ - DescriptorManager(VulkanDevice& device); - - /** - * @brief Destructor. - */ - ~DescriptorManager(); - - /** - * @brief Create the descriptor pool. - * @param maxSets The maximum number of descriptor sets. - * @return True if the descriptor pool was created successfully, false otherwise. - */ - bool createDescriptorPool(uint32_t maxSets); - - /** - * @brief Create uniform buffers for an entity. - * @param entity The entity. - * @param maxFramesInFlight The maximum number of frames in flight. - * @return True if the uniform buffers were created successfully, false otherwise. - */ - bool createUniformBuffers(Entity* entity, uint32_t maxFramesInFlight); - bool update_descriptor_sets(Entity* entity, uint32_t maxFramesInFlight, bool& value1); - - /** - * @brief Create descriptor sets for an entity. - * @param entity The entity. - * @param texturePath The texture path. - * @param descriptorSetLayout The descriptor set layout. - * @param maxFramesInFlight The maximum number of frames in flight. - * @return True if the descriptor sets were created successfully, false otherwise. - */ - bool createDescriptorSets(Entity* entity, const std::string& texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight); - - /** - * @brief Update uniform buffer for an entity. - * @param currentImage The current image index. - * @param entity The entity. - * @param camera The camera. - */ - void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); - - /** - * @brief Get the descriptor pool. - * @return The descriptor pool. - */ - vk::raii::DescriptorPool& getDescriptorPool() { return descriptorPool; } - - /** - * @brief Get the entity resources. - * @return The entity resources. - */ - const std::unordered_map& getEntityResources() { return entityResources; } - - /** - * @brief Get the resources for an entity. - * @param entity The entity. - * @return The entity resources. - */ - const EntityResources& getEntityResources(Entity* entity) { return entityResources[entity]; } - -private: - // Vulkan device - VulkanDevice& device; - - // Descriptor pool - vk::raii::DescriptorPool descriptorPool = nullptr; - std::unordered_map entityResources; - - // Helper functions - std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); +class DescriptorManager +{ + public: + // Entity resources + struct EntityResources + { + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + std::vector uniformBuffersMapped; + std::vector descriptorSets; + }; + + /** + * @brief Constructor. + * @param device The Vulkan device. + */ + DescriptorManager(VulkanDevice &device); + + /** + * @brief Destructor. + */ + ~DescriptorManager(); + + /** + * @brief Create the descriptor pool. + * @param maxSets The maximum number of descriptor sets. + * @return True if the descriptor pool was created successfully, false otherwise. + */ + bool createDescriptorPool(uint32_t maxSets); + + /** + * @brief Create uniform buffers for an entity. + * @param entity The entity. + * @param maxFramesInFlight The maximum number of frames in flight. + * @return True if the uniform buffers were created successfully, false otherwise. + */ + bool createUniformBuffers(Entity *entity, uint32_t maxFramesInFlight); + bool update_descriptor_sets(Entity *entity, uint32_t maxFramesInFlight, bool &value1); + + /** + * @brief Create descriptor sets for an entity. + * @param entity The entity. + * @param texturePath The texture path. + * @param descriptorSetLayout The descriptor set layout. + * @param maxFramesInFlight The maximum number of frames in flight. + * @return True if the descriptor sets were created successfully, false otherwise. + */ + bool createDescriptorSets(Entity *entity, const std::string &texturePath, vk::DescriptorSetLayout descriptorSetLayout, uint32_t maxFramesInFlight); + + /** + * @brief Update uniform buffer for an entity. + * @param currentImage The current image index. + * @param entity The entity. + * @param camera The camera. + */ + void updateUniformBuffer(uint32_t currentImage, Entity *entity, CameraComponent *camera); + + /** + * @brief Get the descriptor pool. + * @return The descriptor pool. + */ + vk::raii::DescriptorPool &getDescriptorPool() + { + return descriptorPool; + } + + /** + * @brief Get the entity resources. + * @return The entity resources. + */ + const std::unordered_map &getEntityResources() + { + return entityResources; + } + + /** + * @brief Get the resources for an entity. + * @param entity The entity. + * @return The entity resources. + */ + const EntityResources &getEntityResources(Entity *entity) + { + return entityResources[entity]; + } + + private: + // Vulkan device + VulkanDevice &device; + + // Descriptor pool + vk::raii::DescriptorPool descriptorPool = nullptr; + std::unordered_map entityResources; + + // Helper functions + std::pair createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties); }; diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 71d6e830..5a455a75 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -1,879 +1,1039 @@ +/* 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 "scene_loading.h" #include "mesh_component.h" +#include "scene_loading.h" -#include #include -#include -#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()) { +Engine::Engine() : + resourceManager(std::make_unique()) +{ } -Engine::~Engine() { - Cleanup(); +Engine::~Engine() +{ + Cleanup(); } -bool Engine::Initialize(const std::string& appName, int width, int height, bool enableValidationLayers) { - // Create platform +bool Engine::Initialize(const std::string &appName, int width, int height, bool enableValidationLayers) +{ + // 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; + // For Android, the platform is created with the android_app + // This will be handled in the android_main function + return false; #else - 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)) { - 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; + 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)) + { + 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; - } - - // 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) { - 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 = "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; - } - - // Update - Update(deltaTimeMs); - - // Render - Render(); - } +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; + } + + // 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) + { + 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 = "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; + } + + // 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 - entities.clear(); - entityMap.clear(); +void Engine::Cleanup() +{ + if (initialized) + { + // Wait for the device to be idle before cleaning up + if (renderer) + { + renderer->WaitIdle(); + } + + // Clear entities + 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; + } +} - // Clean up subsystems in reverse order of creation - imguiSystem.reset(); - physicsSystem.reset(); - audioSystem.reset(); - modelLoader.reset(); - renderer.reset(); - platform.reset(); +Entity *Engine::CreateEntity(const std::string &name) +{ + // 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; +} - initialized = false; - } +Entity *Engine::GetEntity(const std::string &name) +{ + auto it = entityMap.find(name); + if (it != entityMap.end()) + { + return it->second; + } + return nullptr; } -Entity* Engine::CreateEntity(const std::string& name) { - // 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; +bool Engine::RemoveEntity(Entity *entity) +{ + if (!entity) + { + return false; + } + + // 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; +} - return rawPtr; +bool Engine::RemoveEntity(const std::string &name) +{ + Entity *entity = GetEntity(name); + if (entity) + { + return RemoveEntity(entity); + } + return false; } -Entity* Engine::GetEntity(const std::string& name) { - auto it = entityMap.find(name); - if (it != entityMap.end()) { - return it->second; - } - return nullptr; +void Engine::SetActiveCamera(CameraComponent *cameraComponent) +{ + activeCamera = cameraComponent; } -bool Engine::RemoveEntity(Entity* entity) { - if (!entity) { - return false; - } +const CameraComponent *Engine::GetActiveCamera() const +{ + return activeCamera; +} - // Remember the name before erasing ownership - std::string name = entity->GetName(); +const ResourceManager *Engine::GetResourceManager() const +{ + return resourceManager.get(); +} - // Find the entity in the vector - auto it = std::ranges::find_if(entities, - [entity](const std::unique_ptr& e) { - return e.get() == entity; - }); +const Platform *Engine::GetPlatform() const +{ + return platform.get(); +} - if (it != entities.end()) { - // Remove from the vector (ownership) - entities.erase(it); +Renderer *Engine::GetRenderer() +{ + return renderer.get(); +} - // 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; - }); +ModelLoader *Engine::GetModelLoader() +{ + return modelLoader.get(); +} - if (remainingIt != entities.end()) { - entityMap[name] = remainingIt->get(); - } else { - entityMap.erase(name); - } +const AudioSystem *Engine::GetAudioSystem() const +{ + return audioSystem.get(); +} - return true; - } +PhysicsSystem *Engine::GetPhysicsSystem() +{ + return physicsSystem.get(); +} - return false; +const ImGuiSystem *Engine::GetImGuiSystem() const +{ + return imguiSystem.get(); } -bool Engine::RemoveEntity(const std::string& name) { - Entity* entity = GetEntity(name); - if (entity) { - return RemoveEntity(entity); - } - return false; +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) +{ + 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); + } } -void Engine::SetActiveCamera(CameraComponent* cameraComponent) { - activeCamera = cameraComponent; +void Engine::Update(TimeDelta deltaTime) +{ + // 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 (guard against null unique_ptrs) + for (auto &entity : entities) + { + if (!entity) + { + continue; + } + if (!entity->IsActive()) + { + continue; + } + entity->Update(deltaTime); + } } -const CameraComponent* Engine::GetActiveCamera() const { - return activeCamera; +void Engine::Render() +{ + // Ensure renderer is ready + if (!renderer || !renderer->IsInitialized()) + { + return; + } + + // Check if we have an active camera + if (!activeCamera) + { + return; + } + + // Render the scene (ImGui will be rendered within the render pass) + renderer->Render(entities, activeCamera, imguiSystem.get()); } -const ResourceManager* Engine::GetResourceManager() const { - return resourceManager.get(); +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)); } -const Platform* Engine::GetPlatform() const { - return platform.get(); +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)); + } } -Renderer* Engine::GetRenderer() { - return renderer.get(); +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 + auto ballEntityIt = std::ranges::find_if(entities, [](auto const &entity) { return entity->IsActive() && (entity->GetName().find("Ball_") != std::string::npos); }); + Entity *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(); } -ModelLoader* Engine::GetModelLoader() { - return modelLoader.get(); +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; } -const AudioSystem* Engine::GetAudioSystem() const { - return audioSystem.get(); +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); } -PhysicsSystem* Engine::GetPhysicsSystem() { - return physicsSystem.get(); +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; } -const ImGuiSystem* Engine::GetImGuiSystem() const { - return imguiSystem.get(); +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::handleMouseInput(float x, float y, uint32_t buttons) { - // Check if ImGui wants to capture mouse input first - bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); +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); + + // 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(); +} - // Suppress right-click while loading - if (renderer && renderer->IsLoading()) { - buttons &= ~2u; // clear right button bit - } +void Engine::HandleMouseHover(float mouseX, float mouseY) +{ + // Update current mouse position for any systems that might need it + currentMouseX = mouseX; + currentMouseY = mouseY; +} - 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 = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top - cameraControl.lastMouseX = x; - cameraControl.lastMouseY = y; +#if defined(PLATFORM_ANDROID) +// Android-specific implementation +bool Engine::InitializeAndroid(android_app *app, const std::string &appName, bool enableValidationLayers) +{ + // 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 (!renderer->Initialize(appName, enableValidationLayers)) + { + return false; + } + + // Initialize model loader + if (!modelLoader->Initialize(renderer.get())) + { + return false; + } + + // Connect model loader to renderer for light extraction + renderer->SetModelLoader(modelLoader.get()); + + // Initialize audio system + if (!audioSystem->Initialize(this, renderer.get())) + { + return false; + } + + // Initialize physics system + physicsSystem->SetRenderer(renderer.get()); + + // Enable GPU acceleration for physics calculations to drastically speed up computations + physicsSystem->SetGPUAccelerationEnabled(true); + + if (!physicsSystem->Initialize()) + { + return false; + } + + // Get window dimensions from platform + int width, height; + platform->GetWindowSize(&width, &height); + + // Initialize ImGui system + if (!imguiSystem->Initialize(renderer.get(), width, height)) + { + return false; + } + + // Connect ImGui system to audio system for UI controls + imguiSystem->SetAudioSystem(audioSystem.get()); + + // Generate ball material properties once at load time + GenerateBallMaterial(); + + // Initialize physics scaling system + InitializePhysicsScaling(); + + initialized = true; + return true; +} - xOffset *= cameraControl.mouseSensitivity; - yOffset *= cameraControl.mouseSensitivity; +void Engine::RunAndroid() +{ + if (!initialized) + { + throw std::runtime_error("Engine not initialized"); + } - cameraControl.yaw += xOffset; - cameraControl.pitch += yOffset; + running = true; - // 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; - } - } + // Main loop is handled by the platform + // We just need to update and render when the platform is ready - 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) { - 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; - case GLFW_KEY_0: - break; - default: break; - } - - if (imguiSystem) { - imguiSystem->HandleKeyboard(key, pressed); - } -} - -void Engine::Update(TimeDelta deltaTime) { - // 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 (guard against null unique_ptrs) - for (auto& entity : entities) { - if (!entity) { continue; } - if (!entity->IsActive()) { continue; } - entity->Update(deltaTime); - } -} - -void Engine::Render() { - // Ensure renderer is ready - if (!renderer || !renderer->IsInitialized()) { - return; - } - - // Check if we have an active camera - if (!activeCamera) { - return; - } - - // Render the scene (ImGui will be rendered within the render pass) - renderer->Render(entities, activeCamera, imguiSystem.get()); -} - -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) const { - 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 - auto ballEntityIt = std::ranges::find_if( entities, []( auto const & entity ){ return entity->IsActive() && ( entity->GetName().find( "Ball_" ) != std::string::npos ); } ); - Entity* 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; - - // Calculate camera direction vectors based on yaw and pitch - glm::vec3 front; - front.x = cosf(glm::radians(cameraControl.yaw)) * cosf(glm::radians(cameraControl.pitch)); - front.y = sinf(glm::radians(cameraControl.pitch)); - front.z = sinf(glm::radians(cameraControl.yaw)) * cosf(glm::radians(cameraControl.pitch)); - front = glm::normalize(front); - - glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); - glm::vec3 right = glm::normalize(glm::cross(front, up)); - up = glm::normalize(glm::cross(right, front)); - - // 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); - - // Update camera target based on a direction - glm::vec3 target = position + front; - activeCamera->SetTarget(target); -} - -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); - - // 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; -} + // Calculate delta time + deltaTimeMs = CalculateDeltaTimeMs(); + // Update + Update(deltaTimeMs); -#if defined(PLATFORM_ANDROID) -// Android-specific implementation -bool Engine::InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers) { - // 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 (!renderer->Initialize(appName, enableValidationLayers)) { - return false; - } - - // Initialize model loader - if (!modelLoader->Initialize(renderer.get())) { - return false; - } - - // Connect model loader to renderer for light extraction - renderer->SetModelLoader(modelLoader.get()); - - // Initialize audio system - if (!audioSystem->Initialize(this, renderer.get())) { - return false; - } - - // Initialize physics system - physicsSystem->SetRenderer(renderer.get()); - - // Enable GPU acceleration for physics calculations to drastically speed up computations - physicsSystem->SetGPUAccelerationEnabled(true); - - if (!physicsSystem->Initialize()) { - return false; - } - - // Get window dimensions from platform - int width, height; - platform->GetWindowSize(&width, &height); - - // Initialize ImGui system - if (!imguiSystem->Initialize(renderer.get(), width, height)) { - return false; - } - - // Connect ImGui system to audio system for UI controls - imguiSystem->SetAudioSystem(audioSystem.get()); - - // Generate ball material properties once at load time - GenerateBallMaterial(); - - // Initialize physics scaling system - InitializePhysicsScaling(); - - initialized = true; - return true; -} - -void Engine::RunAndroid() { - if (!initialized) { - throw std::runtime_error("Engine not initialized"); - } - - running = true; - - // Main loop is handled by the platform - // We just need to update and render when the platform is ready - - // Calculate delta time - deltaTimeMs = CalculateDeltaTimeMs(); - - // Update - Update(deltaTimeMs); - - // Render - Render(); + // Render + Render(); } #endif diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 21bcd975..329940bd 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -1,20 +1,36 @@ +/* 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 "platform.h" -#include "renderer.h" -#include "resource_manager.h" -#include "entity.h" +#include "audio_system.h" #include "camera_component.h" +#include "entity.h" +#include "imgui_system.h" #include "model_loader.h" -#include "audio_system.h" #include "physics_system.h" -#include "imgui_system.h" +#include "platform.h" +#include "renderer.h" +#include "resource_manager.h" /** * @brief Main engine class that manages the game loop and subsystems. @@ -22,338 +38,350 @@ * 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); - - /** - * @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 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); +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); + + /** + * @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); - #else - bool InitializeAndroid(android_app* app, const std::string& appName, bool enableValidationLayers = true); - #endif - - /** - * @brief Run the engine on Android. - */ - void RunAndroid(); +/** + * @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); +# else + bool InitializeAndroid(android_app *app, const std::string &appName, bool enableValidationLayers = true); +# 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 - std::vector> entities; - std::unordered_map entityMap; - - // 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; // Horizontal rotation - float pitch = 0.0f; // Vertical rotation - bool firstMouse = true; - float cameraSpeed = 5.0f; - float mouseSensitivity = 0.1f; - } 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 Render the scene. - */ - void Render(); - - /** - * @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) const; - - /** - * @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); - - + 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 + std::vector> entities; + std::unordered_map entityMap; + + // 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 Render the scene. + */ + void Render(); + + /** + * @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/simple_engine/entity.cpp b/attachments/simple_engine/entity.cpp index 41292561..125f121c 100644 --- a/attachments/simple_engine/entity.cpp +++ b/attachments/simple_engine/entity.cpp @@ -1,30 +1,56 @@ +/* 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 "entity.h" // Most of the Entity class implementation is in the header file // This file is mainly for any methods that might need additional implementation -void Entity::Initialize() { - for (auto& component : components) { - component->Initialize(); - } +void Entity::Initialize() +{ + for (auto &component : components) + { + component->Initialize(); + } } -void Entity::Update(std::chrono::milliseconds deltaTime) { - if (!active) return; +void Entity::Update(std::chrono::milliseconds deltaTime) +{ + if (!active) + return; - for (auto& component : components) { - if (component->IsActive()) { - component->Update(deltaTime); - } - } + for (auto &component : components) + { + if (component->IsActive()) + { + component->Update(deltaTime); + } + } } -void Entity::Render() { - if (!active) return; +void Entity::Render() +{ + if (!active) + return; - for (auto& component : components) { - if (component->IsActive()) { - component->Render(); - } - } + for (auto &component : components) + { + if (component->IsActive()) + { + component->Render(); + } + } } diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h index 0ef252f2..9f2ec5e4 100644 --- a/attachments/simple_engine/entity.h +++ b/attachments/simple_engine/entity.h @@ -1,11 +1,27 @@ +/* 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 "component.h" @@ -15,130 +31,150 @@ * Entities are containers for components. They don't have any behavior * on their own, but gain functionality through the components attached to them. */ -class Entity { -private: - std::string name; - bool active = true; - std::vector> components; - -public: - /** - * @brief Constructor with a name. - * @param entityName The name of the entity. - */ - explicit Entity(const std::string& entityName) : name(entityName) {} - - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~Entity() = default; - - /** - * @brief Get the name of the entity. - * @return The name of the entity. - */ - const std::string& GetName() const { return name; } - - /** - * @brief Check if the entity is active. - * @return True if the entity is active, false otherwise. - */ - bool IsActive() const { return active; } - - /** - * @brief Set the active state of the entity. - * @param isActive The new active state. - */ - void SetActive(bool isActive) { active = isActive; } - - /** - * @brief Initialize all components of the entity. - */ - void Initialize(); - - /** - * @brief Update all components of the entity. - * @param deltaTime The time elapsed since the last frame. - */ - void Update(std::chrono::milliseconds deltaTime); - - /** - * @brief Render all components of the entity. - */ - void Render(); - - /** - * @brief Add a component to the entity. - * @tparam T The type of component to add. - * @tparam Args The types of arguments to pass to the component constructor. - * @param args The arguments to pass to the component constructor. - * @return A pointer to the newly created component. - */ - template - T* AddComponent(Args&&... args) { - static_assert(std::is_base_of::value, "T must derive from Component"); - - // Create the component - auto component = std::make_unique(std::forward(args)...); - T* componentPtr = component.get(); - - // Set the owner - componentPtr->SetOwner(this); - - // Add to the vector for ownership and iteration - components.push_back(std::move(component)); - - // Initialize the component - componentPtr->Initialize(); - - return componentPtr; - } - - /** - * @brief Get a component of a specific type. - * @tparam T The type of component to get. - * @return A pointer to the component, or nullptr if not found. - */ - template - T* GetComponent() const { - static_assert(std::is_base_of::value, "T must derive from Component"); - - // Search from the back to preserve previous behavior of returning the last-added component of type T - for (auto it = components.rbegin(); it != components.rend(); ++it) { - if (auto* casted = dynamic_cast(it->get())) { - return casted; - } - } - return nullptr; - } - - /** - * @brief Remove a component of a specific type. - * @tparam T The type of component to remove. - * @return True if the component was removed, false otherwise. - */ - template - bool RemoveComponent() { - static_assert(std::is_base_of::value, "T must derive from Component"); - - for (auto it = components.rbegin(); it != components.rend(); ++it) { - if (dynamic_cast(it->get()) != nullptr) { - components.erase(std::next(it).base()); - return true; - } - } - - return false; - } - - /** - * @brief Check if the entity has a component of a specific type. - * @tparam T The type of component to check for. - * @return True if the entity has the component, false otherwise. - */ - template - bool HasComponent() const { - static_assert(std::is_base_of::value, "T must derive from Component"); - return GetComponent() != nullptr; - } +class Entity +{ + private: + std::string name; + bool active = true; + std::vector> components; + + public: + /** + * @brief Constructor with a name. + * @param entityName The name of the entity. + */ + explicit Entity(const std::string &entityName) : + name(entityName) + {} + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Entity() = default; + + /** + * @brief Get the name of the entity. + * @return The name of the entity. + */ + const std::string &GetName() const + { + return name; + } + + /** + * @brief Check if the entity is active. + * @return True if the entity is active, false otherwise. + */ + bool IsActive() const + { + return active; + } + + /** + * @brief Set the active state of the entity. + * @param isActive The new active state. + */ + void SetActive(bool isActive) + { + active = isActive; + } + + /** + * @brief Initialize all components of the entity. + */ + void Initialize(); + + /** + * @brief Update all components of the entity. + * @param deltaTime The time elapsed since the last frame. + */ + void Update(std::chrono::milliseconds deltaTime); + + /** + * @brief Render all components of the entity. + */ + void Render(); + + /** + * @brief Add a component to the entity. + * @tparam T The type of component to add. + * @tparam Args The types of arguments to pass to the component constructor. + * @param args The arguments to pass to the component constructor. + * @return A pointer to the newly created component. + */ + template + T *AddComponent(Args &&...args) + { + static_assert(std::is_base_of::value, "T must derive from Component"); + + // Create the component + auto component = std::make_unique(std::forward(args)...); + T *componentPtr = component.get(); + + // Set the owner + componentPtr->SetOwner(this); + + // Add to the vector for ownership and iteration + components.push_back(std::move(component)); + + // Initialize the component + componentPtr->Initialize(); + + return componentPtr; + } + + /** + * @brief Get a component of a specific type. + * @tparam T The type of component to get. + * @return A pointer to the component, or nullptr if not found. + */ + template + T *GetComponent() const + { + static_assert(std::is_base_of::value, "T must derive from Component"); + + // Search from the back to preserve previous behavior of returning the last-added component of type T + for (auto it = components.rbegin(); it != components.rend(); ++it) + { + if (auto *casted = dynamic_cast(it->get())) + { + return casted; + } + } + return nullptr; + } + + /** + * @brief Remove a component of a specific type. + * @tparam T The type of component to remove. + * @return True if the component was removed, false otherwise. + */ + template + bool RemoveComponent() + { + static_assert(std::is_base_of::value, "T must derive from Component"); + + for (auto it = components.rbegin(); it != components.rend(); ++it) + { + if (dynamic_cast(it->get()) != nullptr) + { + components.erase(std::next(it).base()); + return true; + } + } + + return false; + } + + /** + * @brief Check if the entity has a component of a specific type. + * @tparam T The type of component to check for. + * @return True if the entity has the component, false otherwise. + */ + template + bool HasComponent() const + { + static_assert(std::is_base_of::value, "T must derive from Component"); + return GetComponent() != nullptr; + } }; diff --git a/attachments/simple_engine/imgui/imconfig.h b/attachments/simple_engine/imgui/imconfig.h index 33cbadd1..f2850ff3 100644 --- a/attachments/simple_engine/imgui/imconfig.h +++ b/attachments/simple_engine/imgui/imconfig.h @@ -7,27 +7,27 @@ #pragma once //---- Define assertion handler. Defaults to calling assert(). -//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +// #define IM_ASSERT(_EXPR) MyAssert(_EXPR) //---- Define attributes of all API symbols declarations, e.g. for DLL under Windows. -//#define IMGUI_API __declspec( dllexport ) -//#define IMGUI_API __declspec( dllimport ) +// #define IMGUI_API __declspec( dllexport ) +// #define IMGUI_API __declspec( dllimport ) //---- Include imgui_user.h at the end of imgui.h -//#define IMGUI_INCLUDE_IMGUI_USER_H +// #define IMGUI_INCLUDE_IMGUI_USER_H //---- 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 +// #define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS +// #define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS //---- Don't implement help and test window functionality (ShowUserGuide()/ShowStyleEditor()/ShowTestWindow() methods will be empty) -//#define IMGUI_DISABLE_TEST_WINDOWS +// #define IMGUI_DISABLE_TEST_WINDOWS //---- Don't define obsolete functions names -//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// #define IMGUI_DISABLE_OBSOLETE_FUNCTIONS //---- Implement STB libraries in a namespace to avoid conflicts -//#define IMGUI_STB_NAMESPACE ImGuiStb +// #define IMGUI_STB_NAMESPACE ImGuiStb //---- Define constructor and implicit cast operators to convert back<>forth from your math types and ImVec2/ImVec4. /* @@ -48,4 +48,3 @@ namespace ImGui void Value(const char* prefix, const MyMatrix44& v, const char* float_format = NULL); } */ - diff --git a/attachments/simple_engine/imgui/imgui.cpp b/attachments/simple_engine/imgui/imgui.cpp index c53edb84..f01336af 100644 --- a/attachments/simple_engine/imgui/imgui.cpp +++ b/attachments/simple_engine/imgui/imgui.cpp @@ -49,7 +49,7 @@ - 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 + - 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) Designed for developers and content-creators, not the typical end-user! Some of the weaknesses includes: @@ -94,15 +94,15 @@ 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) - - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. + - 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. + 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. GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE - - Add the Dear ImGui source files to your projects, using your preferred build system. + - 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. @@ -110,7 +110,7 @@ - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. - 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 + (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: @@ -121,7 +121,7 @@ (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 + - 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: @@ -158,7 +158,7 @@ // 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! - + // Render & swap video buffers ImGui::Render(); MyImGuiRenderFunction(ImGui::GetDrawData()); @@ -189,8 +189,8 @@ } 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. + // 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. @@ -207,8 +207,8 @@ } - 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 + - 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] @@ -250,7 +250,7 @@ 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. - - 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/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. @@ -285,9 +285,9 @@ removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. - 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/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/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/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! + - 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. @@ -307,8 +307,8 @@ - 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/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. + - 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) @@ -422,7 +422,7 @@ 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. + - 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). @@ -444,7 +444,7 @@ - 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 + - 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" @@ -532,18 +532,18 @@ 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. + 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. + 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 + 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.) @@ -559,7 +559,7 @@ io.Fonts->AddFontFromFileTTF("MyDataFolder/MyFontFile.ttf", size_in_pixels); // ALSO CORRECT 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 + 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: How can I load multiple fonts? @@ -591,11 +591,11 @@ io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs 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. + 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; @@ -605,7 +605,7 @@ 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. + 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. @@ -614,11 +614,11 @@ 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()' + 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, + 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. @@ -628,11 +628,11 @@ 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. + 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. + - 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 @@ -643,153 +643,172 @@ */ #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -#define _CRT_SECURE_NO_WARNINGS +# define _CRT_SECURE_NO_WARNINGS #endif #include "imgui.h" #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui_internal.h" -#include // toupper, isprint -#include // NULL, malloc, free, qsort, atoi -#include // vsnprintf, sscanf, printf -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t +#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 #else -#include // intptr_t +# include // intptr_t #endif -#define IMGUI_DEBUG_NAV_SCORING 0 -#define IMGUI_DEBUG_NAV_RECTS 0 +#define IMGUI_DEBUG_NAV_SCORING 0 +#define IMGUI_DEBUG_NAV_RECTS 0 // 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 : 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 #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' // +# 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 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 #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 +# define IMGUI_CDECL __cdecl #else -#define IMGUI_CDECL +# define IMGUI_CDECL #endif //------------------------------------------------------------------------- // Forward Declarations //------------------------------------------------------------------------- -static bool IsKeyPressedMap(ImGuiKey key, bool repeat = true); +static bool IsKeyPressedMap(ImGuiKey key, bool repeat = true); -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 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 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); +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); -static ImGuiWindowSettings* AddWindowSettings(const char* name); +static ImGuiWindowSettings *AddWindowSettings(const char *name); -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); +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); -static ImRect GetViewportRect(); +static ImRect GetViewportRect(); -static void ClosePopupToLevel(int remaining); -static ImGuiWindow* GetFrontMostModalRootWindow(); +static void ClosePopupToLevel(int remaining); +static ImGuiWindow *GetFrontMostModalRootWindow(); -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); +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); -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); +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); namespace ImGui { -static void NavUpdate(); -static void NavUpdateWindowing(); -static void NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, const ImGuiID id); +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); -} +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 //----------------------------------------------------------------------------- -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); +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 //----------------------------------------------------------------------------- -// 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. +// 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) #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. +// 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. #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) +{ + (void) user_data; + return malloc(size); +} +static void FreeWrapper(void *ptr, void *user_data) +{ + (void) 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) +{ + (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); +} #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 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; //----------------------------------------------------------------------------- // User facing structures @@ -797,117 +816,120 @@ static size_t GImAllocatorActiveAllocationsCount = 0; 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 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); } // 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 = 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); } 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 + // 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 + OptMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag #else - OptMacOSXBehaviors = false; + OptMacOSXBehaviors = false; #endif - OptCursorBlink = true; + OptCursorBlink = true; - // Settings (User Functions) - GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations - SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; - ClipboardUserData = NULL; - ImeSetInputScreenPosFn = ImeSetInputScreenPosFn_DefaultImpl; - ImeWindowHandle = NULL; + // Settings (User Functions) + GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; + ClipboardUserData = NULL; + ImeSetInputScreenPosFn = ImeSetInputScreenPosFn_DefaultImpl; + ImeWindowHandle = NULL; #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - RenderDrawListsFn = NULL; + RenderDrawListsFn = NULL; #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; + // 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; } // Pass in translated ASCII characters for text input. @@ -915,242 +937,260 @@ ImGuiIO::ImGuiIO() // - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message void ImGuiIO::AddInputCharacter(ImWchar c) { - const int n = ImStrlenW(InputCharacters); - if (n + 1 < IM_ARRAYSIZE(InputCharacters)) - { - InputCharacters[n] = c; - InputCharacters[n+1] = '\0'; - } + const int n = ImStrlenW(InputCharacters); + if (n + 1 < IM_ARRAYSIZE(InputCharacters)) + { + InputCharacters[n] = c; + InputCharacters[n + 1] = '\0'; + } } -void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) +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]); + // 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]); } //----------------------------------------------------------------------------- // HELPERS //----------------------------------------------------------------------------- -#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_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 // 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" +# define IM_NEWLINE "\r\n" #else -#define IM_NEWLINE "\n" +# define IM_NEWLINE "\n" #endif -ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) +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; + 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; } -bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +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)); + 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)); } -void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w) +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; + 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; } -ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +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; + 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; } -int ImStricmp(const char* str1, const char* str2) +int ImStricmp(const char *str1, const char *str2) { - int d; - while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } - return d; + int d; + while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) + { + str1++; + str2++; + } + return d; } -int ImStrnicmp(const char* str1, const char* str2, size_t count) +int ImStrnicmp(const char *str1, const char *str2, size_t count) { - int d = 0; - while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; count--; } - return d; + int d = 0; + while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) + { + str1++; + str2++; + count--; + } + return d; } -void ImStrncpy(char* dst, const char* src, size_t count) +void ImStrncpy(char *dst, const char *src, size_t count) { - if (count < 1) return; - strncpy(dst, src, count); - dst[count-1] = 0; + if (count < 1) + return; + strncpy(dst, src, count); + dst[count - 1] = 0; } -char* ImStrdup(const char *str) +char *ImStrdup(const char *str) { - size_t len = strlen(str) + 1; - void* buf = ImGui::MemAlloc(len); - return (char*)memcpy(buf, (const void*)str, len); + size_t len = strlen(str) + 1; + void *buf = ImGui::MemAlloc(len); + return (char *) memcpy(buf, (const void *) str, len); } -char* ImStrchrRange(const char* str, const char* str_end, char c) +char *ImStrchrRange(const char *str, const char *str_end, char c) { - for ( ; str < str_end; str++) - if (*str == c) - return (char*)str; - return NULL; + for (; str < str_end; str++) + if (*str == c) + return (char *) str; + return NULL; } -int ImStrlenW(const ImWchar* str) +int ImStrlenW(const ImWchar *str) { - int n = 0; - while (*str++) n++; - return n; + int n = 0; + while (*str++) + n++; + return n; } -const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line +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; + while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') + buf_mid_line--; + return buf_mid_line; } -const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) +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); + if (!needle_end) + needle_end = needle + strlen(needle); - 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; + 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; } -static const char* ImAtoi(const char* src, int* output) +static const char *ImAtoi(const char *src, int *output) { - 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; + 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; } -// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). +// 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, ...) +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; + 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 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; + 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 +#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; +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; } //----------------------------------------------------------------------------- @@ -1160,334 +1200,382 @@ ImU32 ImHash(const void* data, int data_size, ImU32 seed) // 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; +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; } // 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; - } +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; + } } static inline int ImTextCountUtf8BytesFromChar(unsigned int c) { - 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; + 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; } -int ImTextStrToUtf8(char* buf, int buf_size, const ImWchar* in_text, const ImWchar* in_text_end) +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) { - 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); + 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); } -int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4 &in) { - 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; + 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; } -ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) { - 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); + ImGuiStyle &style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); } -ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) +ImU32 ImGui::GetColorU32(const ImVec4 &col) { - 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; -} - -ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) -{ - ImGuiStyle& style = GImGui->Style; - ImVec4 c = style.Colors[idx]; - c.w *= style.Alpha * alpha_mul; - return ColorConvertFloat4ToU32(c); -} - -ImU32 ImGui::GetColorU32(const ImVec4& col) -{ - ImGuiStyle& style = GImGui->Style; - ImVec4 c = col; - c.w *= style.Alpha; - return ColorConvertFloat4ToU32(c); + ImGuiStyle &style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); } -const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) -{ - ImGuiStyle& style = GImGui->Style; - return style.Colors[idx]; +const ImVec4 &ImGui::GetStyleColorVec4(ImGuiCol idx) +{ + ImGuiStyle &style = GImGui->Style; + return style.Colors[idx]; } ImU32 ImGui::GetColorU32(ImU32 col) -{ - 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); +{ + 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); } // 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; +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; } // 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) +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]); + // 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); + return fopen(filename, mode); #endif } // 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; +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; } //----------------------------------------------------------------------------- @@ -1496,147 +1584,149 @@ void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, int* //----------------------------------------------------------------------------- // 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; +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; } // 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() { - 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); + 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 ImGuiStorage::GetInt(ImGuiID key, int default_val) const { - ImVector::iterator it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) - return default_val; - return it->val_i; + ImVector::iterator it = LowerBound(const_cast &>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_i; } bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const { - return GetInt(key, default_val ? 1 : 0) != 0; + return GetInt(key, default_val ? 1 : 0) != 0; } float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const { - ImVector::iterator it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) - return default_val; - return it->val_f; + ImVector::iterator it = LowerBound(const_cast &>(Data), key); + if (it == Data.end() || it->key != key) + return default_val; + return it->val_f; } -void* ImGuiStorage::GetVoidPtr(ImGuiID key) const +void *ImGuiStorage::GetVoidPtr(ImGuiID key) const { - ImVector::iterator it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) - return NULL; - return it->val_p; + ImVector::iterator it = LowerBound(const_cast &>(Data), key); + if (it == Data.end() || it->key != key) + return NULL; + return it->val_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 *ImGuiStorage::GetIntRef(ImGuiID key, int default_val) { - 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; + 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; } -bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) +bool *ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) { - return (bool*)GetIntRef(key, default_val ? 1 : 0); + return (bool *) GetIntRef(key, default_val ? 1 : 0); } -float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) +float *ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) { - 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; + 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; } -void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) +void **ImGuiStorage::GetVoidPtrRef(ImGuiID key, void *default_val) { - 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; + 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; } // 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) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_i = val; + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_i = val; } void ImGuiStorage::SetBool(ImGuiID key, bool val) { - SetInt(key, val ? 1 : 0); + SetInt(key, val ? 1 : 0); } void ImGuiStorage::SetFloat(ImGuiID key, float val) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_f = val; + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_f = val; } -void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) +void ImGuiStorage::SetVoidPtr(ImGuiID key, void *val) { - ImVector::iterator it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { - Data.insert(it, Pair(key, val)); - return; - } - it->val_p = val; + ImVector::iterator it = LowerBound(Data, key); + if (it == Data.end() || it->key != key) + { + Data.insert(it, Pair(key, val)); + return; + } + it->val_p = val; } void ImGuiStorage::SetAllInt(int v) { - for (int i = 0; i < Data.Size; i++) - Data[i].val_i = v; + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; } //----------------------------------------------------------------------------- @@ -1644,99 +1734,99 @@ void ImGuiStorage::SetAllInt(int v) //----------------------------------------------------------------------------- // 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)); +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)); } void ImGuiTextFilter::Build() { - 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; + 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; } //----------------------------------------------------------------------------- @@ -1746,37 +1836,37 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const // 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) +# define va_copy(dest, src) (dest = src) #endif // Helper: Text buffer for logging/accumulating text -void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) +void ImGuiTextBuffer::appendfv(const char *fmt, va_list args) { - va_list args_copy; - va_copy(args_copy, args); + va_list args_copy; + va_copy(args_copy, args); - 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; + 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; - 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); - } + 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); + } - Buf.resize(needed_sz); - ImFormatStringV(&Buf[write_off - 1], len + 1, fmt, args_copy); + Buf.resize(needed_sz); + ImFormatStringV(&Buf[write_off - 1], len + 1, fmt, args_copy); } -void ImGuiTextBuffer::appendf(const char* fmt, ...) +void ImGuiTextBuffer::appendf(const char *fmt, ...) { - va_list args; - va_start(args, fmt); - appendfv(fmt, args); - va_end(args); + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); } //----------------------------------------------------------------------------- @@ -1785,43 +1875,44 @@ void ImGuiTextBuffer::appendf(const char* fmt, ...) ImGuiMenuColumns::ImGuiMenuColumns() { - Count = 0; - Spacing = Width = NextWidth = 0.0f; - memset(Pos, 0, sizeof(Pos)); - memset(NextWidths, 0, sizeof(NextWidths)); + Count = 0; + Spacing = Width = NextWidth = 0.0f; + memset(Pos, 0, sizeof(Pos)); + memset(NextWidths, 0, sizeof(NextWidths)); } void ImGuiMenuColumns::Update(int count, float spacing, bool clear) { - 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; - } -} - -float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double -{ - 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); + 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; + } +} + +float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double +{ + 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); } float ImGuiMenuColumns::CalcExtraSpace(float avail_w) { - return ImMax(0.0f, avail_w - Width); + return ImMax(0.0f, avail_w - Width); } //----------------------------------------------------------------------------- @@ -1830,14 +1921,14 @@ float ImGuiMenuColumns::CalcExtraSpace(float avail_w) 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 + // 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 @@ -1845,608 +1936,616 @@ static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height) // 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; - } + 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; + } } void ImGuiListClipper::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; + 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; } bool ImGuiListClipper::Step() { - 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; + 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; } //----------------------------------------------------------------------------- // ImGuiWindow //----------------------------------------------------------------------------- -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; +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; } ImGuiWindow::~ImGuiWindow() { - IM_DELETE(DrawList); - IM_DELETE(Name); - for (int i = 0; i != ColumnsStorage.Size; i++) - ColumnsStorage[i].~ImGuiColumnsSet(); + IM_DELETE(DrawList); + IM_DELETE(Name); + for (int i = 0; i != ColumnsStorage.Size; i++) + ColumnsStorage[i].~ImGuiColumnsSet(); } -ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +ImGuiID ImGuiWindow::GetID(const char *str, const char *str_end) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHash(str, str_end ? (int)(str_end - str) : 0, seed); - ImGui::KeepAliveID(id); - return id; + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHash(str, str_end ? (int) (str_end - str) : 0, seed); + ImGui::KeepAliveID(id); + return id; } -ImGuiID ImGuiWindow::GetID(const void* ptr) +ImGuiID ImGuiWindow::GetID(const void *ptr) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHash(&ptr, sizeof(void*), seed); - ImGui::KeepAliveID(id); - return id; + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHash(&ptr, sizeof(void *), seed); + ImGui::KeepAliveID(id); + return id; } -ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char* str, const char* str_end) +ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char *str, const char *str_end) { - ImGuiID seed = IDStack.back(); - return ImHash(str, str_end ? (int)(str_end - str) : 0, seed); + ImGuiID seed = IDStack.back(); + return ImHash(str, str_end ? (int) (str_end - str) : 0, seed); } // This is only used in rare/specific situations to manufacture an ID out of nowhere. -ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect &r_abs) { - 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; + 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; } //----------------------------------------------------------------------------- // Internal API exposed in imgui_internal.h //----------------------------------------------------------------------------- -static void SetCurrentWindow(ImGuiWindow* window) +static void SetCurrentWindow(ImGuiWindow *window) { - ImGuiContext& g = *GImGui; - g.CurrentWindow = window; - if (window) - g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + ImGuiContext &g = *GImGui; + g.CurrentWindow = window; + if (window) + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); } static void SetNavID(ImGuiID id, int nav_layer) { - ImGuiContext& g = *GImGui; - IM_ASSERT(g.NavWindow); - IM_ASSERT(nav_layer == 0 || nav_layer == 1); - g.NavId = id; - g.NavWindow->NavLastIds[nav_layer] = id; + ImGuiContext &g = *GImGui; + IM_ASSERT(g.NavWindow); + IM_ASSERT(nav_layer == 0 || nav_layer == 1); + g.NavId = id; + g.NavWindow->NavLastIds[nav_layer] = id; } -static void SetNavIDAndMoveMouse(ImGuiID id, int nav_layer, const ImRect& rect_rel) +static void SetNavIDAndMoveMouse(ImGuiID id, int nav_layer, const ImRect &rect_rel) { - ImGuiContext& g = *GImGui; - SetNavID(id, nav_layer); - g.NavWindow->NavRectRel[nav_layer] = rect_rel; - g.NavMousePosDirty = true; - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; + ImGuiContext &g = *GImGui; + SetNavID(id, nav_layer); + g.NavWindow->NavRectRel[nav_layer] = rect_rel; + g.NavMousePosDirty = true; + g.NavDisableHighlight = false; + g.NavDisableMouseHover = true; } -void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow *window) { - 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; - } + 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; + } } ImGuiID ImGui::GetActiveID() { - ImGuiContext& g = *GImGui; - return g.ActiveId; + ImGuiContext &g = *GImGui; + return g.ActiveId; } -void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow *window) { - ImGuiContext& g = *GImGui; - IM_ASSERT(id != 0); + 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); + // 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); - if (g.ActiveIdSource == ImGuiInputSource_Nav) - g.NavDisableMouseHover = true; - else - g.NavDisableHighlight = true; + if (g.ActiveIdSource == ImGuiInputSource_Nav) + g.NavDisableMouseHover = true; + else + g.NavDisableHighlight = true; } void ImGui::ClearActiveID() { - SetActiveID(0, NULL); + SetActiveID(0, NULL); } void ImGui::SetHoveredID(ImGuiID id) { - ImGuiContext& g = *GImGui; - g.HoveredId = id; - g.HoveredIdAllowOverlap = false; - g.HoveredIdTimer = (id != 0 && g.HoveredIdPreviousFrame == id) ? (g.HoveredIdTimer + g.IO.DeltaTime) : 0.0f; + ImGuiContext &g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + g.HoveredIdTimer = (id != 0 && g.HoveredIdPreviousFrame == id) ? (g.HoveredIdTimer + g.IO.DeltaTime) : 0.0f; } ImGuiID ImGui::GetHoveredID() { - ImGuiContext& g = *GImGui; - return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; + ImGuiContext &g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; } void ImGui::KeepAliveID(ImGuiID id) { - ImGuiContext& g = *GImGui; - if (g.ActiveId == id) - g.ActiveIdIsAlive = true; + ImGuiContext &g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = true; } -static inline bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) +static inline bool IsWindowContentHoverable(ImGuiWindow *window, ImGuiHoveredFlags flags) { - // 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; - } + // 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; + } - return true; + return true; } // Advance cursor given item size for layout. -void ImGui::ItemSize(const ImVec2& size, float text_offset_y) +void ImGui::ItemSize(const ImVec2 &size, float text_offset_y) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (window->SkipItems) - return; + 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] + // 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; + 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(); + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); } -void ImGui::ItemSize(const ImRect& bb, float text_offset_y) +void ImGui::ItemSize(const ImRect &bb, float text_offset_y) { - ItemSize(bb.GetSize(), 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; + 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; + 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; - } +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; + } #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 - - // 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; -} + 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 -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; + // 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) +static ImGuiWindow *NavRestoreLastChildNavWindow(ImGuiWindow *window) { - return window->NavLastChildNavWindow ? window->NavLastChildNavWindow : 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); + 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; + 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; + ImGuiContext &g = *GImGui; + return g.NavMoveRequest && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; } void ImGui::NavMoveRequestCancel() { - ImGuiContext& g = *GImGui; - g.NavMoveRequest = false; - NavUpdateAnyRequestFlag(); + ImGuiContext &g = *GImGui; + g.NavMoveRequest = false; + NavUpdateAnyRequestFlag(); } // 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; +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; + // [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); + 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) - } + 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) + } } // 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; +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; } // This is roughly matching the behavior of internal-facing ItemHoverable() @@ -2454,2353 +2553,2386 @@ bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg) // - 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; - ImGuiWindow* window = g.CurrentWindow; - if (g.NavDisableMouseHover && !g.NavDisableHighlight) - return IsItemFocused(); - - // 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 - - // 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; - - // 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; - - // Test if interactions on this window are blocked by an active popup or modal - if (!IsWindowContentHoverable(window, flags)) - return false; - - // Test if the item is disabled - if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) - return false; - - // 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; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; + if (g.NavDisableMouseHover && !g.NavDisableHighlight) + return IsItemFocused(); + + // 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 + + // 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; + + // 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; + + // Test if interactions on this window are blocked by an active popup or modal + if (!IsWindowContentHoverable(window, flags)) + return false; + + // Test if the item is disabled + if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) + return false; + + // 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; } // Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). -bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) +bool ImGui::ItemHoverable(const ImRect &bb, ImGuiID id) { - ImGuiContext& g = *GImGui; - if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) - return false; + ImGuiContext &g = *GImGui; + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; - 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; + 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; - SetHoveredID(id); - return true; + SetHoveredID(id); + return true; } -bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id, bool clip_even_when_logged) +bool ImGui::IsClippedEx(const ImRect &bb, ImGuiID id, bool clip_even_when_logged) { - 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; + 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; } -bool ImGui::FocusableItemRegister(ImGuiWindow* window, ImGuiID id, bool tab_stop) +bool ImGui::FocusableItemRegister(ImGuiWindow *window, ImGuiID id, bool tab_stop) { - ImGuiContext& g = *GImGui; + 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++; + const bool allow_keyboard_focus = (window->DC.ItemFlags & (ImGuiItemFlags_AllowKeyboardFocus | ImGuiItemFlags_Disabled)) == ImGuiItemFlags_AllowKeyboardFocus; + window->FocusIdxAllCounter++; + if (allow_keyboard_focus) + window->FocusIdxTabCounter++; - // 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. + // 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. - if (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent) - return true; - if (allow_keyboard_focus && window->FocusIdxTabCounter == window->FocusIdxTabRequestCurrent) - { - g.NavJustTabbedId = id; - return true; - } + if (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent) + return true; + if (allow_keyboard_focus && window->FocusIdxTabCounter == window->FocusIdxTabRequestCurrent) + { + g.NavJustTabbedId = id; + return true; + } - return false; + return false; } -void ImGui::FocusableItemUnregister(ImGuiWindow* window) +void ImGui::FocusableItemUnregister(ImGuiWindow *window) { - window->FocusIdxAllCounter--; - window->FocusIdxTabCounter--; + window->FocusIdxAllCounter--; + window->FocusIdxTabCounter--; } ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_x, float default_y) { - 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; + 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; } -float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +float ImGui::CalcWrapWidthForPos(const ImVec2 &pos, float wrap_pos_x) { - if (wrap_pos_x < 0.0f) - return 0.0f; + if (wrap_pos_x < 0.0f) + return 0.0f; - 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 + 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 - return ImMax(wrap_pos_x - pos.x, 1.0f); + return ImMax(wrap_pos_x - pos.x, 1.0f); } //----------------------------------------------------------------------------- -void* ImGui::MemAlloc(size_t sz) +void *ImGui::MemAlloc(size_t sz) { - GImAllocatorActiveAllocationsCount++; - return GImAllocatorAllocFunc(sz, GImAllocatorUserData); + GImAllocatorActiveAllocationsCount++; + return GImAllocatorAllocFunc(sz, GImAllocatorUserData); } -void ImGui::MemFree(void* ptr) +void ImGui::MemFree(void *ptr) { - if (ptr) GImAllocatorActiveAllocationsCount--; - return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); + if (ptr) + GImAllocatorActiveAllocationsCount--; + return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); } -const char* ImGui::GetClipboardText() +const char *ImGui::GetClipboardText() { - return GImGui->IO.GetClipboardTextFn ? GImGui->IO.GetClipboardTextFn(GImGui->IO.ClipboardUserData) : ""; + return GImGui->IO.GetClipboardTextFn ? GImGui->IO.GetClipboardTextFn(GImGui->IO.ClipboardUserData) : ""; } -void ImGui::SetClipboardText(const char* text) +void ImGui::SetClipboardText(const char *text) { - if (GImGui->IO.SetClipboardTextFn) - GImGui->IO.SetClipboardTextFn(GImGui->IO.ClipboardUserData, text); + if (GImGui->IO.SetClipboardTextFn) + GImGui->IO.SetClipboardTextFn(GImGui->IO.ClipboardUserData, text); } -const char* ImGui::GetVersion() +const char *ImGui::GetVersion() { - return IMGUI_VERSION; + return IMGUI_VERSION; } // 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() +ImGuiContext *ImGui::GetCurrentContext() { - return GImGui; + return GImGui; } -void ImGui::SetCurrentContext(ImGuiContext* ctx) +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. + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. #else - GImGui = ctx; + GImGui = ctx; #endif } -void ImGui::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void(*free_func)(void* ptr, void* user_data), void* user_data) +void ImGui::SetAllocatorFunctions(void *(*alloc_func)(size_t sz, void *user_data), void (*free_func)(void *ptr, void *user_data), void *user_data) { - GImAllocatorAllocFunc = alloc_func; - GImAllocatorFreeFunc = free_func; - GImAllocatorUserData = user_data; + GImAllocatorAllocFunc = alloc_func; + GImAllocatorFreeFunc = free_func; + GImAllocatorUserData = user_data; } -ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +ImGuiContext *ImGui::CreateContext(ImFontAtlas *shared_font_atlas) { - ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); - if (GImGui == NULL) - SetCurrentContext(ctx); - Initialize(ctx); - return ctx; + ImGuiContext *ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + if (GImGui == NULL) + SetCurrentContext(ctx); + Initialize(ctx); + return ctx; } -void ImGui::DestroyContext(ImGuiContext* ctx) +void ImGui::DestroyContext(ImGuiContext *ctx) { - if (ctx == NULL) - ctx = GImGui; - Shutdown(ctx); - if (GImGui == ctx) - SetCurrentContext(NULL); - IM_DELETE(ctx); + if (ctx == NULL) + ctx = GImGui; + Shutdown(ctx); + if (GImGui == ctx) + SetCurrentContext(NULL); + IM_DELETE(ctx); } -ImGuiIO& ImGui::GetIO() +ImGuiIO &ImGui::GetIO() { - IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); - return GImGui->IO; + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + return GImGui->IO; } -ImGuiStyle& ImGui::GetStyle() +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() or 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() +ImDrawData *ImGui::GetDrawData() { - ImGuiContext& g = *GImGui; - return g.DrawData.Valid ? &g.DrawData : NULL; + ImGuiContext &g = *GImGui; + return g.DrawData.Valid ? &g.DrawData : NULL; } float ImGui::GetTime() { - return GImGui->Time; + return GImGui->Time; } int ImGui::GetFrameCount() { - return GImGui->FrameCount; + return GImGui->FrameCount; } -ImDrawList* ImGui::GetOverlayDrawList() +ImDrawList *ImGui::GetOverlayDrawList() { - return &GImGui->OverlayDrawList; + return &GImGui->OverlayDrawList; } -ImDrawListSharedData* ImGui::GetDrawListSharedData() +ImDrawListSharedData *ImGui::GetDrawListSharedData() { - return &GImGui->DrawListSharedData; + return &GImGui->DrawListSharedData; } // 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); - 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]; - } +void ImGui::NavInitWindow(ImGuiWindow *window, bool force_reinit) +{ + 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]; + } } static ImVec2 NavCalcPreferredMousePos() { - 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; + 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. } -static int FindWindowIndex(ImGuiWindow* window) // FIXME-OPT O(N) +static int FindWindowIndex(ImGuiWindow *window) // FIXME-OPT O(N) { - ImGuiContext& g = *GImGui; - for (int i = g.Windows.Size-1; i >= 0; i--) - if (g.Windows[i] == window) - return i; - return -1; + ImGuiContext &g = *GImGui; + for (int i = g.Windows.Size - 1; i >= 0; i--) + if (g.Windows[i] == window) + return i; + return -1; } -static ImGuiWindow* FindWindowNavigable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +static ImGuiWindow *FindWindowNavigable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) { - 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; + 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; } float ImGui::GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode) { - 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; + 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; } // Equivalent of IsKeyDown() for NavInputs[] static bool IsNavInputDown(ImGuiNavInput n) { - return GImGui->IO.NavInputs[n] > 0.0f; + return GImGui->IO.NavInputs[n] > 0.0f; } // Equivalent of IsKeyPressed() for NavInputs[] static bool IsNavInputPressed(ImGuiNavInput n, ImGuiInputReadMode mode) { - return ImGui::GetNavInputAmount(n, mode) > 0.0f; + return ImGui::GetNavInputAmount(n, mode) > 0.0f; } static bool IsNavInputPressedAnyOfTwo(ImGuiNavInput n1, ImGuiNavInput n2, ImGuiInputReadMode mode) { - return (ImGui::GetNavInputAmount(n1, mode) + ImGui::GetNavInputAmount(n2, mode)) > 0.0f; + return (ImGui::GetNavInputAmount(n1, mode) + ImGui::GetNavInputAmount(n2, mode)) > 0.0f; } ImVec2 ImGui::GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor, float fast_factor) { - 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; + 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; } static void NavUpdateWindowingHighlightWindow(int focus_change_dir) { - ImGuiContext& g = *GImGui; - IM_ASSERT(g.NavWindowingTarget); - if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) - return; + 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; + 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; } // Window management mode (hold to: change focus/move/resize, tap to: toggle menu layer) static void ImGui::NavUpdateWindowing() { - 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); - } + 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); +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); } static void ImGui::NavUpdate() { - ImGuiContext& g = *GImGui; - g.IO.WantMoveMouse = false; + 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 - // 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; + // 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 +} - 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; +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; + } +} - // 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; +void ImGui::NewFrame() +{ + 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; +} - // 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; - } +// 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; +} - // 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; - } +// 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); +} - // 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); +void ImGui::MarkIniSettingsDirty() +{ + ImGuiContext &g = *GImGui; + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} - // 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; +static void MarkIniSettingsDirty(ImGuiWindow *window) +{ + ImGuiContext &g = *GImGui; + if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} - NavUpdateWindowing(); +// 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); +} - // 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<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 -} - -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; - } -} - -void ImGui::NewFrame() -{ - 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; -} - -// 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; -} - -// 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); -} - -void ImGui::MarkIniSettingsDirty() -{ - ImGuiContext& g = *GImGui; - if (g.SettingsDirtyTimer <= 0.0f) - g.SettingsDirtyTimer = g.IO.IniSavingRate; -} - -static void MarkIniSettingsDirty(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) - if (g.SettingsDirtyTimer <= 0.0f) - g.SettingsDirtyTimer = g.IO.IniSavingRate; -} - -// 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; - } -} +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) +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(); + 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::PopClipRect() { - ImGuiWindow* window = GetCurrentWindow(); - window->DrawList->PopClipRect(); - window->ClipRect = window->DrawList->_ClipRectStack.back(); + ImGuiWindow *window = GetCurrentWindow(); + window->DrawList->PopClipRect(); + window->ClipRect = window->DrawList->_ClipRectStack.back(); } // 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; - 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; + 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; } void ImGui::Render() { - 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() + 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); + if (g.DrawData.CmdListsCount > 0 && g.IO.RenderDrawListsFn != NULL) + g.IO.RenderDrawListsFn(&g.DrawData); #endif - } + } } -const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) +const char *ImGui::FindRenderedTextEnd(const char *text, const char *text_end) { - const char* text_display_end = text; - if (!text_end) - text_end = (const char*)-1; + 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; + 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; } // Pass text data straight to log (without being displayed) -void ImGui::LogText(const char* fmt, ...) +void ImGui::LogText(const char *fmt, ...) { - ImGuiContext& g = *GImGui; - if (!g.LogEnabled) - return; + 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); + va_list args; + va_start(args, fmt); + if (g.LogFile) + { + vfprintf(g.LogFile, fmt, args); + } + else + { + g.LogClipboard->appendfv(fmt, args); + } + va_end(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 -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; - } +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; + } } // 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); - } +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); + } } // 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::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); } // 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) { - 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); - } + 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); + } } void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) { - 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); - } + 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)); + 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); + 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); - } + 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); + } } // 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) +ImVec2 ImGui::CalcTextSize(const char *text, const char *text_end, bool hide_text_after_double_hash, float wrap_width) { - ImGuiContext& g = *GImGui; + 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; + 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); + 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); + // 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; + 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; +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; } // 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() +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; + 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; } // 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) +bool ImGui::IsMouseHoveringRect(const ImVec2 &r_min, const ImVec2 &r_max, bool clip) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; - // Clip - ImRect rect_clipped(r_min, r_max); - if (clip) - rect_clipped.ClipWith(window->ClipRect); + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(window->ClipRect); - // 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); + // 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); } static bool IsKeyPressedMap(ImGuiKey key, bool repeat) { - const int key_index = GImGui->IO.KeyMap[key]; - return (key_index >= 0) ? ImGui::IsKeyPressed(key_index, repeat) : false; + const int key_index = GImGui->IO.KeyMap[key]; + return (key_index >= 0) ? ImGui::IsKeyPressed(key_index, repeat) : false; } int ImGui::GetKeyIndex(ImGuiKey imgui_key) { - IM_ASSERT(imgui_key >= 0 && imgui_key < ImGuiKey_COUNT); - return GImGui->IO.KeyMap[imgui_key]; + IM_ASSERT(imgui_key >= 0 && imgui_key < ImGuiKey_COUNT); + return GImGui->IO.KeyMap[imgui_key]; } // 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) { - 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]; + 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]; } int ImGui::CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate) { - 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; + 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; } int ImGui::GetKeyPressedAmount(int key_index, float repeat_delay, float repeat_rate) { - 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); + 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); } bool ImGui::IsKeyPressed(int user_key_index, bool repeat) { - 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; + 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; } bool ImGui::IsKeyReleased(int user_key_index) { - 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; + 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]; } bool ImGui::IsMouseDown(int button) { - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDown[button]; + ImGuiContext &g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button]; } 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; + ImGuiContext &g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; } bool ImGui::IsMouseClicked(int button, bool repeat) { - 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; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + const float t = g.IO.MouseDownDuration[button]; + if (t == 0.0f) + return true; - 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; - } + 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; + } - return false; + return false; } bool ImGui::IsMouseReleased(int button) { - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseReleased[button]; + ImGuiContext &g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button]; } bool ImGui::IsMouseDoubleClicked(int button) { - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDoubleClicked[button]; + ImGuiContext &g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDoubleClicked[button]; } bool ImGui::IsMouseDragging(int button, float lock_threshold) { - 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; + 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; } ImVec2 ImGui::GetMousePos() { - return GImGui->IO.MousePos; + return GImGui->IO.MousePos; } // NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() { - ImGuiContext& g = *GImGui; - if (g.CurrentPopupStack.Size > 0) - return g.OpenPopupStack[g.CurrentPopupStack.Size-1].OpenMousePos; - return g.IO.MousePos; + ImGuiContext &g = *GImGui; + if (g.CurrentPopupStack.Size > 0) + return g.OpenPopupStack[g.CurrentPopupStack.Size - 1].OpenMousePos; + return g.IO.MousePos; } // We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position -bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +bool ImGui::IsMousePosValid(const ImVec2 *mouse_pos) { - 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; + 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; } // 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) { - 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); + 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); } void ImGui::ResetMouseDragDelta(int button) { - 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; + 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; } ImGuiMouseCursor ImGui::GetMouseCursor() { - return GImGui->MouseCursor; + return GImGui->MouseCursor; } void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) { - GImGui->MouseCursor = cursor_type; + GImGui->MouseCursor = cursor_type; } void ImGui::CaptureKeyboardFromApp(bool capture) { - GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; + GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; } void ImGui::CaptureMouseFromApp(bool capture) { - GImGui->WantCaptureMouseNextFrame = capture ? 1 : 0; + GImGui->WantCaptureMouseNextFrame = capture ? 1 : 0; } bool ImGui::IsItemActive() { - ImGuiContext& g = *GImGui; - if (g.ActiveId) - { - ImGuiWindow* window = g.CurrentWindow; - return g.ActiveId == window->DC.LastItemId; - } - return false; + ImGuiContext &g = *GImGui; + if (g.ActiveId) + { + ImGuiWindow *window = g.CurrentWindow; + return g.ActiveId == window->DC.LastItemId; + } + return false; } bool ImGui::IsItemFocused() { - ImGuiContext& g = *GImGui; - return g.NavId && !g.NavDisableHighlight && g.NavId == g.CurrentWindow->DC.LastItemId; + ImGuiContext &g = *GImGui; + return g.NavId && !g.NavDisableHighlight && g.NavId == g.CurrentWindow->DC.LastItemId; } bool ImGui::IsItemClicked(int mouse_button) { - return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_Default); + return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_Default); } bool ImGui::IsAnyItemHovered() { - ImGuiContext& g = *GImGui; - return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; + ImGuiContext &g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; } bool ImGui::IsAnyItemActive() { - ImGuiContext& g = *GImGui; - return g.ActiveId != 0; + ImGuiContext &g = *GImGui; + return g.ActiveId != 0; } bool ImGui::IsAnyItemFocused() { - ImGuiContext& g = *GImGui; - return g.NavId != 0 && !g.NavDisableHighlight; + ImGuiContext &g = *GImGui; + return g.NavId != 0 && !g.NavDisableHighlight; } bool ImGui::IsItemVisible() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(window->DC.LastItemRect); + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(window->DC.LastItemRect); } // 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() { - ImGuiContext& g = *GImGui; - if (g.HoveredId == g.CurrentWindow->DC.LastItemId) - g.HoveredIdAllowOverlap = true; - if (g.ActiveId == g.CurrentWindow->DC.LastItemId) - g.ActiveIdAllowOverlap = true; + ImGuiContext &g = *GImGui; + if (g.HoveredId == g.CurrentWindow->DC.LastItemId) + g.HoveredIdAllowOverlap = true; + if (g.ActiveId == g.CurrentWindow->DC.LastItemId) + g.ActiveIdAllowOverlap = true; } ImVec2 ImGui::GetItemRectMin() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Min; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Min; } ImVec2 ImGui::GetItemRectMax() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Max; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Max; } ImVec2 ImGui::GetItemRectSize() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.GetSize(); + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.LastItemRect.GetSize(); } static ImRect GetViewportRect() { - 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); + 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); } // 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) { - 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); + 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); } -void ImGui::SetTooltipV(const char* fmt, va_list args) +void ImGui::SetTooltipV(const char *fmt, va_list args) { - BeginTooltipEx(0, true); - TextV(fmt, args); - EndTooltip(); + BeginTooltipEx(0, true); + TextV(fmt, args); + EndTooltip(); } -void ImGui::SetTooltip(const char* fmt, ...) +void ImGui::SetTooltip(const char *fmt, ...) { - va_list args; - va_start(args, fmt); - SetTooltipV(fmt, args); - va_end(args); + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); } void ImGui::BeginTooltip() { - BeginTooltipEx(0, false); + BeginTooltipEx(0, false); } void ImGui::EndTooltip() { - IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls - End(); + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); } // Mark popup as open (toggle toward open state). @@ -4809,770 +4941,842 @@ void ImGui::EndTooltip() // 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; + 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); + 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); + if (!IsPopupOpen(id)) + return; + ImGuiContext &g = *GImGui; + ClosePopupToLevel(g.OpenPopupStack.Size - 1); } // Close the popup we have begin-ed into. void ImGui::CloseCurrentPopup() { - 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; + 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); } bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) { - 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 (!IsPopupOpen(id)) + { + g.NextWindowData.Clear(); // We behave like Begin() and need to consume those values + return false; + } - 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 + 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 - 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(); + 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(); - return is_open; + return is_open; } -bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) +bool ImGui::BeginPopup(const char *str_id, ImGuiWindowFlags flags) { - 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); + 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); } bool ImGui::IsPopupOpen(ImGuiID id) { - ImGuiContext& g = *GImGui; - return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == id; + ImGuiContext &g = *GImGui; + return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == id; } -bool ImGui::IsPopupOpen(const char* str_id) +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); + ImGuiContext &g = *GImGui; + return g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].PopupId == g.CurrentWindow->GetID(str_id); } -bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) +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; - } + 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; + } - // 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)); + // 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)); - 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; - } + 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; + } - return is_open; + return is_open; } -static void NavProcessMoveRequestWrapAround(ImGuiWindow* window) +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; - } + 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; + } } 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); + ImGuiContext &g = *GImGui; + (void) g; + IM_ASSERT(g.CurrentWindow->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + IM_ASSERT(g.CurrentPopupStack.Size > 0); + + // Make all menus and popups wrap around for now, may need to expose that policy. + NavProcessMoveRequestWrapAround(g.CurrentWindow); - // Make all menus and popups wrap around for now, may need to expose that policy. - NavProcessMoveRequestWrapAround(g.CurrentWindow); - - End(); + End(); } -bool ImGui::OpenPopupOnItemClick(const char* str_id, int mouse_button) +bool ImGui::OpenPopupOnItemClick(const char *str_id, int mouse_button) { - 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; + 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; } // 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); -} - -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); -} - -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); -} - -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 - - 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); - - const float backup_border_size = g.Style.ChildBorderSize; - if (!border) - g.Style.ChildBorderSize = 0.0f; - flags |= extra_flags; - - 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); - - 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; - - // 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; - } - - return ret; -} - -bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - ImGuiWindow* window = GetCurrentWindow(); - return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); -} - -bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) +bool ImGui::BeginPopupContextItem(const char *str_id, int mouse_button) { - IM_ASSERT(id != 0); - return BeginChildEx(NULL, id, size_arg, border, extra_flags); + 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); } -void ImGui::EndChild() -{ - 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() +bool ImGui::BeginPopupContextWindow(const char *str_id, int mouse_button, bool also_over_items) { - EndChild(); - PopStyleVar(3); - PopStyleColor(); + 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); } -// Save and compare stack sizes on Begin()/End() to detect usage errors -static void CheckStacksSize(ImGuiWindow* window, bool write) +bool ImGui::BeginPopupContextVoid(const char *str_id, int mouse_button) { - // 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)); + 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); } -enum ImGuiPopupPositionPolicy +static bool BeginChildEx(const char *name, ImGuiID id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_flags) { - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox -}; + 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 -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; + 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); - // 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)); + const float backup_border_size = g.Style.ChildBorderSize; + if (!border) + g.Style.ChildBorderSize = 0.0f; + flags |= extra_flags; - // 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; - } - } + 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); - // 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; - } + 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; - // 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; -} + // 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; + } -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); + return ret; } -ImGuiWindow* ImGui::FindWindowByName(const char* name) +bool ImGui::BeginChild(const char *str_id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_flags) { - ImGuiContext& g = *GImGui; - ImGuiID id = ImHash(name, 0); - return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); + ImGuiWindow *window = GetCurrentWindow(); + return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); } -static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags) +bool ImGui::BeginChild(ImGuiID id, const ImVec2 &size_arg, bool border, ImGuiWindowFlags extra_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; + IM_ASSERT(id != 0); + return BeginChildEx(NULL, id, size_arg, border, extra_flags); } -static ImVec2 CalcSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) +void ImGui::EndChild() { - 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; + 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); + } + } } -static ImVec2 CalcSizeContents(ImGuiWindow* window) +// 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) { - 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; + 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); } -static ImVec2 CalcSizeAutoFit(ImGuiWindow* window, const ImVec2& size_contents) +void ImGui::EndChildFrame() { - 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; + EndChild(); + PopStyleVar(3); + PopStyleColor(); } -static float GetScrollMaxX(ImGuiWindow* window) -{ - return ImMax(0.0f, window->SizeContents.x - (window->SizeFull.x - window->ScrollbarSizes.x)); +// 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)); } -static float GetScrollMaxY(ImGuiWindow* window) +enum ImGuiPopupPositionPolicy { - return ImMax(0.0f, window->SizeContents.y - (window->SizeFull.y - window->ScrollbarSizes.y)); -} + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox +}; -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 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; + 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) +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; + 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; } struct ImGuiResizeGripDef { - ImVec2 CornerPos; - ImVec2 InnerDir; - int AngleMin12, AngleMax12; + 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 + { + {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 ImRect GetBorderRect(ImGuiWindow *window, int border_n, float perp_padding, float thickness) { - 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(); + 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(); } // 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; +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. @@ -5582,657 +5786,661 @@ static void ImGui::UpdateManualResize(ImGuiWindow* window, const ImVec2& size_au // 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; +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) +bool ImGui::Begin(const char *name, bool *p_open, const ImVec2 &size_first_use, float bg_alpha_override, ImGuiWindowFlags flags) { - // 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: 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); + // 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); + return ImGui::Begin(name, p_open, flags); } -#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS +#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS void ImGui::End() { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; - if (window->DC.ColumnsSet != NULL) - EndColumns(); - PopClipRect(); // Inner window clip rectangle + 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(); + // 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()); + // 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 @@ -6242,2490 +6450,2588 @@ void ImGui::End() // - 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; - } + 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; + } } // Moving window to front of display and set focus (which happens to be back of our sorted list) -void ImGui::FocusWindow(ImGuiWindow* window) +void ImGui::FocusWindow(ImGuiWindow *window) { - ImGuiContext& g = *GImGui; + 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; - } + 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; + // 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; + // Move the root window to the top of the pile + if (window->RootWindow) + window = window->RootWindow; - // 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(); + // 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(); - // Bring to front - if (!(window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) - BringWindowToFront(window); + // Bring to front + if (!(window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) + BringWindowToFront(window); } -void ImGui::FocusFrontMostActiveWindow(ImGuiWindow* ignore_window) +void ImGui::FocusFrontMostActiveWindow(ImGuiWindow *ignore_window) { - 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; - } + 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; + } } void ImGui::PushItemWidth(float item_width) { - 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->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); } void ImGui::PushMultiItemsWidths(int components, float w_full) { - 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(); + 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(); } void ImGui::PopItemWidth() { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.ItemWidthStack.pop_back(); - window->DC.ItemWidth = window->DC.ItemWidthStack.empty() ? window->ItemWidthDefault : window->DC.ItemWidthStack.back(); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.ItemWidthStack.pop_back(); + window->DC.ItemWidth = window->DC.ItemWidthStack.empty() ? window->ItemWidthDefault : window->DC.ItemWidthStack.back(); } 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; + 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; } -static ImFont* GetDefaultFont() +static ImFont *GetDefaultFont() { - ImGuiContext& g = *GImGui; - return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; + ImGuiContext &g = *GImGui; + return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } -void ImGui::SetCurrentFont(ImFont* font) +void ImGui::SetCurrentFont(ImFont *font) { - 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; + 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; + ImFontAtlas *atlas = g.Font->ContainerAtlas; + g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.Font = g.Font; + g.DrawListSharedData.FontSize = g.FontSize; } -void ImGui::PushFont(ImFont* font) +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); + ImGuiContext &g = *GImGui; + if (!font) + font = GetDefaultFont(); + SetCurrentFont(font); + g.FontStack.push_back(font); + g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); } -void ImGui::PopFont() +void ImGui::PopFont() { - ImGuiContext& g = *GImGui; - g.CurrentWindow->DrawList->PopTextureID(); - g.FontStack.pop_back(); - SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); + ImGuiContext &g = *GImGui; + g.CurrentWindow->DrawList->PopTextureID(); + g.FontStack.pop_back(); + SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); } 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); + ImGuiWindow *window = GetCurrentWindow(); + if (enabled) + window->DC.ItemFlags |= option; + else + window->DC.ItemFlags &= ~option; + window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); } void ImGui::PopItemFlag() { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.ItemFlagsStack.pop_back(); - window->DC.ItemFlags = window->DC.ItemFlagsStack.empty() ? ImGuiItemFlags_Default_ : window->DC.ItemFlagsStack.back(); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.ItemFlagsStack.pop_back(); + window->DC.ItemFlags = window->DC.ItemFlagsStack.empty() ? ImGuiItemFlags_Default_ : window->DC.ItemFlagsStack.back(); } void ImGui::PushAllowKeyboardFocus(bool allow_keyboard_focus) { - PushItemFlag(ImGuiItemFlags_AllowKeyboardFocus, allow_keyboard_focus); + PushItemFlag(ImGuiItemFlags_AllowKeyboardFocus, allow_keyboard_focus); } void ImGui::PopAllowKeyboardFocus() { - PopItemFlag(); + PopItemFlag(); } void ImGui::PushButtonRepeat(bool repeat) { - PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); + PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); } void ImGui::PopButtonRepeat() { - PopItemFlag(); + PopItemFlag(); } void ImGui::PushTextWrapPos(float wrap_pos_x) { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.TextWrapPos = wrap_pos_x; - window->DC.TextWrapPosStack.push_back(wrap_pos_x); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.TextWrapPos = wrap_pos_x; + window->DC.TextWrapPosStack.push_back(wrap_pos_x); } void ImGui::PopTextWrapPos() { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.TextWrapPosStack.pop_back(); - window->DC.TextWrapPos = window->DC.TextWrapPosStack.empty() ? -1.0f : window->DC.TextWrapPosStack.back(); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.TextWrapPosStack.pop_back(); + window->DC.TextWrapPos = window->DC.TextWrapPosStack.empty() ? -1.0f : window->DC.TextWrapPosStack.back(); } // 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); + 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); } -void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) +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; + ImGuiContext &g = *GImGui; + ImGuiColMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorModifiers.push_back(backup); + g.Style.Colors[idx] = col; } 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--; - } + ImGuiContext &g = *GImGui; + while (count > 0) + { + ImGuiColMod &backup = g.ColorModifiers.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorModifiers.pop_back(); + count--; + } } struct ImGuiStyleVarInfo { - ImGuiDataType Type; - ImU32 Offset; - void* GetVarPtr(ImGuiStyle* style) const { return (void*)((unsigned char*)style + Offset); } + ImGuiDataType Type; + ImU32 Offset; + void *GetVarPtr(ImGuiStyle *style) const + { + return (void *) ((unsigned char *) style + Offset); + } }; 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 + { + {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 }; -static const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx) +static const ImGuiStyleVarInfo *GetStyleVarInfo(ImGuiStyleVar idx) { - IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_Count_); - IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_Count_); - return &GStyleVarInfo[idx]; + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_Count_); + IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_Count_); + return &GStyleVarInfo[idx]; } 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. + 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. } -void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2 &val) { - 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. + 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. } 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; + 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; } 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; + 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; } bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) { - ImGuiContext& g = *GImGui; - IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() + ImGuiContext &g = *GImGui; + IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() - if (flags & ImGuiFocusedFlags_AnyWindow) - return g.NavWindow != NULL; + if (flags & ImGuiFocusedFlags_AnyWindow) + return g.NavWindow != NULL; - 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; - } + 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; + } } // Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) -bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +bool ImGui::IsWindowNavFocusable(ImGuiWindow *window) { - ImGuiContext& g = *GImGui; - return window->Active && window == window->RootWindowForTabbing && (!(window->Flags & ImGuiWindowFlags_NoNavFocus) || window == g.NavWindow); + ImGuiContext &g = *GImGui; + return window->Active && window == window->RootWindowForTabbing && (!(window->Flags & ImGuiWindowFlags_NoNavFocus) || window == g.NavWindow); } float ImGui::GetWindowWidth() { - ImGuiWindow* window = GImGui->CurrentWindow; - return window->Size.x; + ImGuiWindow *window = GImGui->CurrentWindow; + return window->Size.x; } float ImGui::GetWindowHeight() { - ImGuiWindow* window = GImGui->CurrentWindow; - return window->Size.y; + ImGuiWindow *window = GImGui->CurrentWindow; + return window->Size.y; } ImVec2 ImGui::GetWindowPos() { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - return window->Pos; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; + return window->Pos; } -static void SetWindowScrollX(ImGuiWindow* window, float new_scroll_x) +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; + 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; } -static void SetWindowScrollY(ImGuiWindow* window, float new_scroll_y) +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; + 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; } -static void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) +static void 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; - window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); - window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); + // 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); - // 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. + // 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. } -void ImGui::SetWindowPos(const ImVec2& pos, ImGuiCond cond) +void ImGui::SetWindowPos(const ImVec2 &pos, ImGuiCond cond) { - ImGuiWindow* window = GetCurrentWindowRead(); - SetWindowPos(window, pos, cond); + ImGuiWindow *window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); } -void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) +void ImGui::SetWindowPos(const char *name, const ImVec2 &pos, ImGuiCond cond) { - if (ImGuiWindow* window = FindWindowByName(name)) - SetWindowPos(window, pos, cond); + if (ImGuiWindow *window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); } ImVec2 ImGui::GetWindowSize() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->Size; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->Size; } -static void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) +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); + // 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); - // 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; - } + // 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; + } } -void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) +void ImGui::SetWindowSize(const ImVec2 &size, ImGuiCond cond) { - SetWindowSize(GImGui->CurrentWindow, size, cond); + SetWindowSize(GImGui->CurrentWindow, size, cond); } -void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) +void ImGui::SetWindowSize(const char *name, const ImVec2 &size, ImGuiCond cond) { - if (ImGuiWindow* window = FindWindowByName(name)) - SetWindowSize(window, size, cond); + if (ImGuiWindow *window = FindWindowByName(name)) + SetWindowSize(window, size, cond); } -static void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond) +static void SetWindowCollapsed(ImGuiWindow *window, bool collapsed, ImGuiCond cond) { - // 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); + // 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; + // Set + window->Collapsed = collapsed; } void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) { - SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); } bool ImGui::IsWindowCollapsed() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->Collapsed; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->Collapsed; } bool ImGui::IsWindowAppearing() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->Appearing; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->Appearing; } -void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) +void ImGui::SetWindowCollapsed(const char *name, bool collapsed, ImGuiCond cond) { - if (ImGuiWindow* window = FindWindowByName(name)) - SetWindowCollapsed(window, collapsed, cond); + if (ImGuiWindow *window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); } void ImGui::SetWindowFocus() { - FocusWindow(GImGui->CurrentWindow); + FocusWindow(GImGui->CurrentWindow); } -void ImGui::SetWindowFocus(const char* name) +void ImGui::SetWindowFocus(const char *name) { - if (name) - { - if (ImGuiWindow* window = FindWindowByName(name)) - FocusWindow(window); - } - else - { - FocusWindow(NULL); - } + if (name) + { + if (ImGuiWindow *window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } } -void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) +void ImGui::SetNextWindowPos(const ImVec2 &pos, ImGuiCond cond, const ImVec2 &pivot) { - ImGuiContext& g = *GImGui; - g.NextWindowData.PosVal = pos; - g.NextWindowData.PosPivotVal = pivot; - g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; + ImGuiContext &g = *GImGui; + g.NextWindowData.PosVal = pos; + g.NextWindowData.PosPivotVal = pivot; + g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; } -void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) +void ImGui::SetNextWindowSize(const ImVec2 &size, ImGuiCond cond) { - ImGuiContext& g = *GImGui; - g.NextWindowData.SizeVal = size; - g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; + ImGuiContext &g = *GImGui; + g.NextWindowData.SizeVal = size; + g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; } -void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) +void ImGui::SetNextWindowSizeConstraints(const ImVec2 &size_min, const ImVec2 &size_max, ImGuiSizeCallback custom_callback, void *custom_callback_user_data) { - 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; + 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; } -void ImGui::SetNextWindowContentSize(const ImVec2& size) +void ImGui::SetNextWindowContentSize(const ImVec2 &size) { - 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; + 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; } void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) { - ImGuiContext& g = *GImGui; - g.NextWindowData.CollapsedVal = collapsed; - g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; + ImGuiContext &g = *GImGui; + g.NextWindowData.CollapsedVal = collapsed; + g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; } void ImGui::SetNextWindowFocus() { - 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; + g.NextWindowData.FocusCond = ImGuiCond_Always; // Using a Cond member for consistency (may transition all of them to single flag set for fast Clear() op) } void ImGui::SetNextWindowBgAlpha(float alpha) { - 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; + 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) } // In window space (not screen space!) ImVec2 ImGui::GetContentRegionMax() { - 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; + 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; } ImVec2 ImGui::GetContentRegionAvail() { - ImGuiWindow* window = GetCurrentWindowRead(); - return GetContentRegionMax() - (window->DC.CursorPos - window->Pos); + ImGuiWindow *window = GetCurrentWindowRead(); + return GetContentRegionMax() - (window->DC.CursorPos - window->Pos); } float ImGui::GetContentRegionAvailWidth() { - return GetContentRegionAvail().x; + return GetContentRegionAvail().x; } // In window space (not screen space!) ImVec2 ImGui::GetWindowContentRegionMin() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Min; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Min; } ImVec2 ImGui::GetWindowContentRegionMax() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Max; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max; } float ImGui::GetWindowContentRegionWidth() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ContentsRegionRect.Max.x - window->ContentsRegionRect.Min.x; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max.x - window->ContentsRegionRect.Min.x; } float ImGui::GetTextLineHeight() { - ImGuiContext& g = *GImGui; - return g.FontSize; + ImGuiContext &g = *GImGui; + return g.FontSize; } float ImGui::GetTextLineHeightWithSpacing() { - ImGuiContext& g = *GImGui; - return g.FontSize + g.Style.ItemSpacing.y; + 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; + 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; + ImGuiContext &g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; } -ImDrawList* ImGui::GetWindowDrawList() +ImDrawList *ImGui::GetWindowDrawList() { - ImGuiWindow* window = GetCurrentWindow(); - return window->DrawList; + ImGuiWindow *window = GetCurrentWindow(); + return window->DrawList; } -ImFont* ImGui::GetFont() +ImFont *ImGui::GetFont() { - return GImGui->Font; + return GImGui->Font; } float ImGui::GetFontSize() { - return GImGui->FontSize; + return GImGui->FontSize; } ImVec2 ImGui::GetFontTexUvWhitePixel() { - return GImGui->DrawListSharedData.TexUvWhitePixel; + return GImGui->DrawListSharedData.TexUvWhitePixel; } void ImGui::SetWindowFontScale(float scale) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); - window->FontWindowScale = scale; - g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + ImGuiContext &g = *GImGui; + ImGuiWindow *window = GetCurrentWindow(); + window->FontWindowScale = scale; + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); } // 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; + 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; + 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; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; } -void ImGui::SetCursorPos(const ImVec2& local_pos) +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); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); } 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); + 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); } 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); + 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); } ImVec2 ImGui::GetCursorStartPos() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.CursorStartPos - window->Pos; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; } ImVec2 ImGui::GetCursorScreenPos() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.CursorPos; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.CursorPos; } -void ImGui::SetCursorScreenPos(const ImVec2& screen_pos) +void ImGui::SetCursorScreenPos(const ImVec2 &screen_pos) { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.CursorPos = screen_pos; - window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.CursorPos = screen_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); } float ImGui::GetScrollX() { - return GImGui->CurrentWindow->Scroll.x; + return GImGui->CurrentWindow->Scroll.x; } float ImGui::GetScrollY() { - return GImGui->CurrentWindow->Scroll.y; + return GImGui->CurrentWindow->Scroll.y; } float ImGui::GetScrollMaxX() { - return GetScrollMaxX(GImGui->CurrentWindow); + return GetScrollMaxX(GImGui->CurrentWindow); } float ImGui::GetScrollMaxY() { - return GetScrollMaxY(GImGui->CurrentWindow); + return GetScrollMaxY(GImGui->CurrentWindow); } void ImGui::SetScrollX(float scroll_x) { - ImGuiWindow* window = GetCurrentWindow(); - window->ScrollTarget.x = scroll_x; - window->ScrollTargetCenterRatio.x = 0.0f; + ImGuiWindow *window = GetCurrentWindow(); + window->ScrollTarget.x = scroll_x; + window->ScrollTargetCenterRatio.x = 0.0f; } void ImGui::SetScrollY(float scroll_y) { - 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; + 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; } void ImGui::SetScrollFromPosY(float pos_y, float center_y_ratio) { - // 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; + // 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; + // 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; } // 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) { - 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); + 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); } void ImGui::ActivateItem(ImGuiID id) { - ImGuiContext& g = *GImGui; - g.NavNextActivateId = id; + ImGuiContext &g = *GImGui; + g.NavNextActivateId = id; } void ImGui::SetKeyboardFocusHere(int offset) { - IM_ASSERT(offset >= -1); // -1 is allowed but not below - ImGuiWindow* window = GetCurrentWindow(); - window->FocusIdxAllRequestNext = window->FocusIdxAllCounter + 1 + offset; - window->FocusIdxTabRequestNext = INT_MAX; + IM_ASSERT(offset >= -1); // -1 is allowed but not below + ImGuiWindow *window = GetCurrentWindow(); + window->FocusIdxAllRequestNext = window->FocusIdxAllCounter + 1 + offset; + window->FocusIdxTabRequestNext = INT_MAX; } void ImGui::SetItemDefaultFocus() { - 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(); - } -} - -void ImGui::SetStateStorage(ImGuiStorage* tree) -{ - ImGuiWindow* window = GetCurrentWindow(); - window->DC.StateStorage = tree ? tree : &window->StateStorage; -} - -ImGuiStorage* ImGui::GetStateStorage() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.StateStorage; + 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(); + } } -void ImGui::TextV(const char* fmt, va_list args) +void ImGui::SetStateStorage(ImGuiStorage *tree) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext& g = *GImGui; - const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - TextUnformatted(g.TempBuffer, text_end); + ImGuiWindow *window = GetCurrentWindow(); + window->DC.StateStorage = tree ? tree : &window->StateStorage; } -void ImGui::Text(const char* fmt, ...) +ImGuiStorage *ImGui::GetStateStorage() { - va_list args; - va_start(args, fmt); - TextV(fmt, args); - va_end(args); + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.StateStorage; } -void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) +void ImGui::TextV(const char *fmt, va_list args) { - PushStyleColor(ImGuiCol_Text, col); - TextV(fmt, args); - PopStyleColor(); -} + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; -void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) -{ - va_list args; - va_start(args, fmt); - TextColoredV(col, fmt, args); - va_end(args); + ImGuiContext &g = *GImGui; + const char *text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + TextUnformatted(g.TempBuffer, text_end); } -void ImGui::TextDisabledV(const char* fmt, va_list args) +void ImGui::Text(const char *fmt, ...) { - PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); - TextV(fmt, args); - PopStyleColor(); + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); } -void ImGui::TextDisabled(const char* fmt, ...) +void ImGui::TextColoredV(const ImVec4 &col, const char *fmt, va_list args) { - va_list args; - va_start(args, fmt); - TextDisabledV(fmt, args); - va_end(args); + PushStyleColor(ImGuiCol_Text, col); + TextV(fmt, args); + PopStyleColor(); } -void ImGui::TextWrappedV(const char* fmt, va_list args) +void ImGui::TextColored(const ImVec4 &col, const char *fmt, ...) { - 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(); + va_list args; + va_start(args, fmt); + TextColoredV(col, fmt, args); + va_end(args); } -void ImGui::TextWrapped(const char* fmt, ...) +void ImGui::TextDisabledV(const char *fmt, va_list args) { - va_list args; - va_start(args, fmt); - TextWrappedV(fmt, args); - va_end(args); + PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); + TextV(fmt, args); + PopStyleColor(); } -void ImGui::TextUnformatted(const char* text, const char* text_end) +void ImGui::TextDisabled(const char *fmt, ...) { - 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); - - 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); - } + 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); + + 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; + 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); + 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); +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) +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; + 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) +bool ImGui::InvisibleButton(const char *str_id, const ImVec2 &size_arg) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + 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; + 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); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); - return pressed; + return pressed; } // Button to close a window -bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos, float radius) +bool ImGui::CloseButton(ImGuiID id, const ImVec2 &pos, float radius) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; - // 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); + // 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); - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); - if (is_clipped) - return pressed; + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + if (is_clipped) + return pressed; - // 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); + // 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); - 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; + 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; } // [Internal] bool ImGui::ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (window->SkipItems) - return false; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; + if (window->SkipItems) + return false; - const ImGuiStyle& style = g.Style; + const ImGuiStyle &style = g.Style; - 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; + 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; - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, 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); + 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); - return pressed; + return pressed; } -void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +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; + 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; + 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)); - } + 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) +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; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; + 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(); + // 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; + 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); + 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)); + // 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; + 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; + 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; + 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; +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; + 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; + IM_ASSERT(g.LogFile == NULL); + g.LogFile = NULL; + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; } 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; -} + 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; +} // 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); + 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; + 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) +bool ImGui::CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen, label); + return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen, label); } -bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags) +bool ImGui::CollapsingHeader(const char *label, bool *p_open, ImGuiTreeNodeFlags flags) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - if (p_open && !*p_open) - 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(); - } + 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(); + } - return is_open; + return is_open; } -bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) +bool ImGui::TreeNodeEx(const char *label, ImGuiTreeNodeFlags flags) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - return TreeNodeBehavior(window->GetID(label), flags, label, NULL); + return TreeNodeBehavior(window->GetID(label), flags, label, NULL); } -bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +bool ImGui::TreeNodeExV(const char *str_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + 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; + 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); } -bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +bool ImGui::TreeNodeExV(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, va_list args) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + 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(ptr_id), flags, g.TempBuffer, label_end); + 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); } -bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) +bool ImGui::TreeNodeV(const char *str_id, const char *fmt, va_list args) { - return TreeNodeExV(str_id, 0, fmt, args); + return TreeNodeExV(str_id, 0, fmt, args); } -bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) +bool ImGui::TreeNodeV(const void *ptr_id, const char *fmt, va_list args) { - return TreeNodeExV(ptr_id, 0, fmt, args); + return TreeNodeExV(ptr_id, 0, fmt, args); } -bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +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; + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, flags, fmt, args); + va_end(args); + return is_open; } -bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +bool ImGui::TreeNodeEx(const void *ptr_id, ImGuiTreeNodeFlags flags, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); - va_end(args); - return is_open; + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); + va_end(args); + return is_open; } -bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) +bool ImGui::TreeNode(const char *str_id, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(str_id, 0, fmt, args); - va_end(args); - return is_open; + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, 0, fmt, args); + va_end(args); + return is_open; } -bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) +bool ImGui::TreeNode(const void *ptr_id, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); - va_end(args); - return is_open; + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); + va_end(args); + return is_open; } -bool ImGui::TreeNode(const char* label) +bool ImGui::TreeNode(const char *label) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; - return TreeNodeBehavior(window->GetID(label), 0, label, NULL); + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; + return TreeNodeBehavior(window->GetID(label), 0, label, NULL); } void ImGui::TreeAdvanceToLabelPos() { - ImGuiContext& g = *GImGui; - g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing(); + ImGuiContext &g = *GImGui; + g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing(); } // Horizontal distance preceding label when using TreeNode() or Bullet() float ImGui::GetTreeNodeToLabelSpacing() { - ImGuiContext& g = *GImGui; - return g.FontSize + (g.Style.FramePadding.x * 2.0f); + ImGuiContext &g = *GImGui; + return g.FontSize + (g.Style.FramePadding.x * 2.0f); } void ImGui::SetNextTreeNodeOpen(bool is_open, ImGuiCond cond) { - ImGuiContext& g = *GImGui; - if (g.CurrentWindow->SkipItems) - return; - g.NextTreeNodeOpenVal = is_open; - g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always; + ImGuiContext &g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextTreeNodeOpenVal = is_open; + g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always; } -void ImGui::PushID(const char* str_id) +void ImGui::PushID(const char *str_id) { - ImGuiWindow* window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(str_id)); + ImGuiWindow *window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(str_id)); } -void ImGui::PushID(const char* str_id_begin, const char* str_id_end) +void ImGui::PushID(const char *str_id_begin, const char *str_id_end) { - ImGuiWindow* window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(str_id_begin, str_id_end)); + ImGuiWindow *window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(str_id_begin, str_id_end)); } -void ImGui::PushID(const void* ptr_id) +void ImGui::PushID(const void *ptr_id) { - ImGuiWindow* window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(ptr_id)); + ImGuiWindow *window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(ptr_id)); } void ImGui::PushID(int int_id) { - const void* ptr_id = (void*)(intptr_t)int_id; - ImGuiWindow* window = GetCurrentWindowRead(); - window->IDStack.push_back(window->GetID(ptr_id)); + const void *ptr_id = (void *) (intptr_t) int_id; + ImGuiWindow *window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(ptr_id)); } void ImGui::PopID() { - ImGuiWindow* window = GetCurrentWindowRead(); - window->IDStack.pop_back(); + ImGuiWindow *window = GetCurrentWindowRead(); + window->IDStack.pop_back(); } -ImGuiID ImGui::GetID(const char* str_id) +ImGuiID ImGui::GetID(const char *str_id) { - return GImGui->CurrentWindow->GetID(str_id); + return GImGui->CurrentWindow->GetID(str_id); } -ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +ImGuiID ImGui::GetID(const char *str_id_begin, const char *str_id_end) { - return GImGui->CurrentWindow->GetID(str_id_begin, str_id_end); + return GImGui->CurrentWindow->GetID(str_id_begin, str_id_end); } -ImGuiID ImGui::GetID(const void* ptr_id) +ImGuiID ImGui::GetID(const void *ptr_id) { - return GImGui->CurrentWindow->GetID(ptr_id); + return GImGui->CurrentWindow->GetID(ptr_id); } void ImGui::Bullet() { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; - 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; - } + 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; + } - // 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); + // 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); } // Text with a little bullet aligned to the typical tree node. -void ImGui::BulletTextV(const char* fmt, va_list args) -{ - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - - 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; - } +void ImGui::BulletTextV(const char *fmt, va_list args) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext &g = *GImGui; + const ImGuiStyle &style = g.Style; + + 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; +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; +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; +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); + 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; + // 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; + 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. @@ -8733,3534 +9039,3700 @@ bool ImGui::SliderBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v // "%.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) +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 (!display_format) + display_format = "%.3f"; + int decimal_precision = ParseFormatPrecision(display_format, 3); + + 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); + } + + // Actual slider behavior + render grab + bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision, ImGuiSliderFlags_Vertical); + + // 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); + + return value_changed; +} + +bool ImGui::SliderAngle(const char *label, float *v_rad, float v_degrees_min, float v_degrees_max) +{ + 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; +} + +bool ImGui::SliderInt(const char *label, int *v, int v_min, int v_max, const char *display_format) { - 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); + 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; +} - // 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); +bool ImGui::VSliderInt(const char *label, const ImVec2 &size, int *v, int v_min, int v_max, const char *display_format) +{ + 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; +} - // 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); +// 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) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - // 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)); + 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(); - if (label_size.x > 0.0f) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); - return value_changed; + 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) +bool ImGui::SliderFloat2(const char *label, float v[2], 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); + return SliderFloatN(label, v, 2, v_min, v_max, display_format, power); +} - 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)); +bool ImGui::SliderFloat3(const char *label, float v[3], float v_min, float v_max, const char *display_format, float power) +{ + return SliderFloatN(label, v, 3, v_min, v_max, display_format, power); +} - ItemSize(bb, style.FramePadding.y); - if (!ItemAdd(frame_bb, id)) - return false; - const bool hovered = ItemHoverable(frame_bb, id); +bool ImGui::SliderFloat4(const char *label, float v[4], float v_min, float v_max, const char *display_format, float power) +{ + return SliderFloatN(label, v, 4, v_min, v_max, display_format, power); +} - if (!display_format) - display_format = "%.3f"; - int decimal_precision = ParseFormatPrecision(display_format, 3); +bool ImGui::SliderIntN(const char *label, int *v, int components, int v_min, int v_max, const char *display_format) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - 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); - } + 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(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::SliderInt2(const char *label, int v[2], int v_min, int v_max, const char *display_format) +{ + return SliderIntN(label, v, 2, v_min, v_max, display_format); +} + +bool ImGui::SliderInt3(const char *label, int v[3], int v_min, int v_max, const char *display_format) +{ + return SliderIntN(label, v, 3, v_min, v_max, display_format); +} - // Actual slider behavior + render grab - bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision, ImGuiSliderFlags_Vertical); +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); +} + +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) +{ + 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(); - // 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); + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); - return value_changed; + return value_changed; } -bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max) +bool ImGui::DragFloat2(const char *label, float v[2], float v_speed, float v_min, float v_max, const char *display_format, float power) { - 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 DragFloatN(label, v, 2, v_speed, v_min, v_max, display_format, power); } -bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* display_format) +bool ImGui::DragFloat3(const char *label, float v[3], float v_speed, float v_min, float v_max, const char *display_format, float power) { - 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; + return DragFloatN(label, v, 3, v_speed, v_min, v_max, display_format, power); } -bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* display_format) +bool ImGui::DragFloat4(const char *label, float v[4], float v_speed, float v_min, float v_max, const char *display_format, float power) { - 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; + return DragFloatN(label, v, 4, v_speed, v_min, v_max, display_format, power); } -// 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) -{ - 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 |= SliderFloat("##v", &v[i], v_min, v_max, display_format, power); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); +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) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); + ImGuiContext &g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2); - return value_changed; -} + 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); -bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* display_format, float power) -{ - return SliderFloatN(label, v, 2, v_min, v_max, display_format, power); -} + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); -bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* display_format, float power) -{ - return SliderFloatN(label, v, 3, v_min, v_max, display_format, power); + return value_changed; } -bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* display_format, float power) +// 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) { - return SliderFloatN(label, v, 4, v_min, v_max, display_format, power); + 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; } -bool ImGui::SliderIntN(const char* label, int* v, int components, int v_min, int v_max, const char* display_format) +bool ImGui::DragIntN(const char *label, int *v, int components, float v_speed, int v_min, int v_max, const char *display_format) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + 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 |= SliderInt("##v", &v[i], v_min, v_max, display_format); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); + 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(); - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); - return value_changed; + return value_changed; } -bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* display_format) +bool ImGui::DragInt2(const char *label, int v[2], float v_speed, int v_min, int v_max, const char *display_format) { - return SliderIntN(label, v, 2, v_min, v_max, display_format); + return DragIntN(label, v, 2, v_speed, v_min, v_max, display_format); } -bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* display_format) +bool ImGui::DragInt3(const char *label, int v[3], float v_speed, int v_min, int v_max, const char *display_format) { - return SliderIntN(label, v, 3, v_min, v_max, display_format); + return DragIntN(label, v, 3, v_speed, 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) +bool ImGui::DragInt4(const char *label, int v[4], float v_speed, int v_min, int v_max, const char *display_format) { - return SliderIntN(label, v, 4, v_min, v_max, display_format); + return DragIntN(label, v, 4, v_speed, v_min, v_max, display_format); } -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) +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) { - 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; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext &g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2); + + 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); + + 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); } -bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* display_format, float power) +struct ImGuiPlotArrayGetterData { - 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); + const float *Values; + int Stride; - // 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; -} + ImGuiPlotArrayGetterData(const float *values, int stride) + { + Values = values; + Stride = stride; + } +}; -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) +static float Plot_ArrayGetter(void *data, int idx) { - 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(); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - - return value_changed; + ImGuiPlotArrayGetterData *plot_data = (ImGuiPlotArrayGetterData *) data; + const float v = *(float *) (void *) ((unsigned char *) plot_data->Values + (size_t) idx * plot_data->Stride); + return v; } -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::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) { - return DragFloatN(label, v, 2, v_speed, v_min, v_max, display_format, power); + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void *) &data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); } -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::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) { - return DragFloatN(label, v, 3, v_speed, v_min, v_max, display_format, power); + PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); } -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::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) { - return DragFloatN(label, v, 4, v_speed, v_min, v_max, display_format, power); + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void *) &data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); } -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) +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) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext& g = *GImGui; - PushID(label); - BeginGroup(); - PushMultiItemsWidths(2); + PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} - 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); +// 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) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext &g = *GImGui; + const ImGuiStyle &style = g.Style; + + 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; + + // 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); + + // 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; + } + + 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); +} + +bool ImGui::Checkbox(const char *label, bool *v) +{ + 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); + + 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)); + } + + if (!ItemAdd(total_bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + *v = !(*v); + + 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); + } + + if (g.LogEnabled) + LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]"); + if (label_size.x > 0.0f) + RenderText(text_bb.Min, label); + + return pressed; +} + +bool ImGui::CheckboxFlags(const char *label, unsigned int *flags, unsigned int flags_value) +{ + 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); + } + + if (g.LogEnabled) + LogRenderedText(&text_bb.Min, active ? "(x)" : "( )"); + if (label_size.x > 0.0f) + RenderText(text_bb.Min, label); + + return pressed; +} + +bool ImGui::RadioButton(const char *label, int *v, int v_button) +{ + const bool pressed = RadioButton(label, *v == v_button); + if (pressed) + { + *v = v_button; + } + return pressed; +} + +static int InputTextCalcTextLenAndLineCount(const char *text_begin, const char **out_text_end) +{ + 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; +} - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - PopID(); +static ImVec2 InputTextCalcTextSizeW(const ImWchar *text_begin, const ImWchar *text_end, const ImWchar **remaining, ImVec2 *out_offset, bool stop_on_new_line) +{ + 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 + + if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n + text_size.y += line_height; - return value_changed; + if (remaining) + *remaining = s; + + return text_size; } -// 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) +// 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 { - 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; -} -bool ImGui::DragIntN(const char* label, int* v, int components, float v_speed, int v_min, int v_max, const char* display_format) +static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING *obj) { - 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 |= DragInt("##v", &v[i], v_speed, v_min, v_max, display_format); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - - return value_changed; + return obj->CurLenW; } - -bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* display_format) +static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING *obj, int idx) { - return DragIntN(label, v, 2, v_speed, v_min, v_max, display_format); + return obj->Text[idx]; } - -bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* display_format) +static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING *obj, int line_start_idx, int char_idx) { - return DragIntN(label, v, 3, v_speed, v_min, v_max, display_format); + 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); } - -bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* display_format) +static int STB_TEXTEDIT_KEYTOTEXT(int key) { - return DragIntN(label, v, 4, v_speed, v_min, v_max, display_format); + return key >= 0x10000 ? 0 : key; } - -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) +static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; +static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow *r, STB_TEXTEDIT_STRING *obj, int line_start_idx) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; - - ImGuiContext& g = *GImGui; - PushID(label); - BeginGroup(); - PushMultiItemsWidths(2); - - 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; + 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)); } -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) +static bool is_separator(unsigned int c) { - 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); - - 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); + return ImCharIsSpace(c) || c == ',' || c == ';' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c == ']' || c == '|'; } - -struct ImGuiPlotArrayGetterData +static int is_word_boundary_from_right(STB_TEXTEDIT_STRING *obj, int idx) { - const float* Values; - int Stride; - - ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } -}; - -static float Plot_ArrayGetter(void* data, int idx) + return idx > 0 ? (is_separator(obj->Text[idx - 1]) && !is_separator(obj->Text[idx])) : 1; +} +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) { - ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; - const float v = *(float*)(void*)((unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); - return v; + idx--; + while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) + idx--; + return idx < 0 ? 0 : idx; } - -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) +#ifdef __APPLE__ // FIXME: Move setting to IO structure +static int is_word_boundary_from_left(STB_TEXTEDIT_STRING *obj, int idx) { - ImGuiPlotArrayGetterData data(values, stride); - PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + return idx > 0 ? (!is_separator(obj->Text[idx - 1]) && is_separator(obj->Text[idx])) : 1; } - -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) +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) { - PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + idx++; + int len = obj->CurLenW; + while (idx < len && !is_word_boundary_from_left(obj, idx)) + idx++; + return idx > len ? len : idx; } - -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) +#else +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING *obj, int idx) { - ImGuiPlotArrayGetterData data(values, stride); - PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); + idx++; + int len = obj->CurLenW; + while (idx < len && !is_word_boundary_from_right(obj, idx)) + idx++; + return idx > len ? len : idx; } +#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 -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) +static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING *obj, int pos, int n) { - PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); -} + ImWchar *dst = obj->Text.Data + pos; -// 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) -{ - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - - 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; - - // 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); - - // 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; - } + // We maintain our buffer length in both UTF-8 and wchar formats + obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); + obj->CurLenW -= n; - 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); + // Offset remaining text + const ImWchar *src = obj->Text.Data + pos + n; + while (ImWchar c = *src++) + *dst++ = c; + *dst = '\0'; } -bool ImGui::Checkbox(const char* label, bool* v) +static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING *obj, int pos, const ImWchar *new_text, int new_text_len) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return false; + const int text_len = obj->CurLenW; + IM_ASSERT(pos <= text_len); + if (new_text_len + text_len + 1 > obj->Text.Size) + 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 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; - 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); + 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)); - 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)); - } + obj->CurLenW += new_text_len; + obj->CurLenA += new_text_len_utf8; + obj->Text[obj->CurLenW] = '\0'; - if (!ItemAdd(total_bb, id)) - return false; + return true; +} - bool hovered, held; - bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) - *v = !(*v); +// 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 - 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); - } +#define STB_TEXTEDIT_IMPLEMENTATION +#include "stb_textedit.h" - if (g.LogEnabled) - LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]"); - if (label_size.x > 0.0f) - RenderText(text_bb.Min, label); +} // namespace ImGuiStb - return pressed; +void ImGuiTextEditState::OnKeyPressed(int key) +{ + stb_textedit_key(this, &StbState, key); + CursorFollow = true; + CursorAnimReset(); } -bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +// 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) { - bool v = ((*flags & flags_value) == flags_value); - bool pressed = Checkbox(label, &v); - if (pressed) - { - if (v) - *flags |= flags_value; - else - *flags &= ~flags_value; - } + 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'; - return pressed; + if (CursorPos + bytes_count >= pos) + CursorPos -= bytes_count; + else if (CursorPos >= pos) + CursorPos = pos; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen -= bytes_count; } -bool ImGui::RadioButton(const char* label, bool active) +void ImGuiTextEditCallbackData::InsertChars(int pos, const char *new_text, const char *new_text_end) { - 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); - } + 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 (g.LogEnabled) - LogRenderedText(&text_bb.Min, active ? "(x)" : "( )"); - if (label_size.x > 0.0f) - RenderText(text_bb.Min, label); + 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'; - return pressed; + if (CursorPos >= pos) + CursorPos += new_text_len; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen += new_text_len; } -bool ImGui::RadioButton(const char* label, int* v, int v_button) -{ - const bool pressed = RadioButton(label, *v == v_button); - if (pressed) - { - *v = v_button; - } - return pressed; +// 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; } -static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) -{ - 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; +// 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); } -static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) -{ - ImFont* font = GImGui->Font; - const float line_height = GImGui->FontSize; - const float scale = line_height / font->FontSize; +// 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); - ImVec2 text_size = ImVec2(0,0); - float line_width = 0.0f; + 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)); - 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; - } + char buf[64]; + DataTypeFormatString(data_type, data_ptr, scalar_format, buf, IM_ARRAYSIZE(buf)); - if (text_size.x < line_width) - text_size.x = line_width; + 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); - if (out_offset) - *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + // 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(); - if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n - text_size.y += line_height; - - if (remaining) - *remaining = s; - - return text_size; -} - -// 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 -{ - -static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return obj->CurLenW; } -static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->Text[idx]; } -static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { 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); } -static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; } -static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; -static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx) -{ - 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)); -} - -static bool is_separator(unsigned int c) { return ImCharIsSpace(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|'; } -static int is_word_boundary_from_right(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (is_separator( obj->Text[idx-1] ) && !is_separator( obj->Text[idx] ) ) : 1; } -static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } -#ifdef __APPLE__ // FIXME: Move setting to IO structure -static int is_word_boundary_from_left(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (!is_separator( obj->Text[idx-1] ) && is_separator( obj->Text[idx] ) ) : 1; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } -#else -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } -#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) -{ - 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'; -} - -static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const ImWchar* new_text, int new_text_len) -{ - 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'; - - return true; -} - -// 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" - -} - -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(); - - 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(); - - return value_changed; -} - -bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags extra_flags) -{ - 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); -} - -bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags extra_flags) -{ - // 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); -} - -bool ImGui::InputFloatN(const char* label, float* v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags) -{ - 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 |= InputFloat("##v", &v[i], 0, 0, decimal_precision, extra_flags); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - - return value_changed; -} - -bool ImGui::InputFloat2(const char* label, float v[2], int decimal_precision, ImGuiInputTextFlags extra_flags) -{ - return InputFloatN(label, v, 2, decimal_precision, extra_flags); -} - -bool ImGui::InputFloat3(const char* label, float v[3], int decimal_precision, ImGuiInputTextFlags extra_flags) -{ - return InputFloatN(label, v, 3, decimal_precision, extra_flags); -} - -bool ImGui::InputFloat4(const char* label, float v[4], int decimal_precision, ImGuiInputTextFlags extra_flags) -{ - return InputFloatN(label, v, 4, decimal_precision, extra_flags); -} - -bool ImGui::InputIntN(const char* label, int* v, int components, ImGuiInputTextFlags extra_flags) -{ - 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 |= InputInt("##v", &v[i], 0, 0, extra_flags); - SameLine(0, g.Style.ItemInnerSpacing.x); - PopID(); - PopItemWidth(); - } - PopID(); - - TextUnformatted(label, FindRenderedTextEnd(label)); - EndGroup(); - - return value_changed; -} - -bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags extra_flags) -{ - return InputIntN(label, v, 2, extra_flags); -} - -bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags extra_flags) -{ - return InputIntN(label, v, 3, extra_flags); -} - -bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags extra_flags) -{ - return InputIntN(label, v, 4, extra_flags); -} - -static float CalcMaxPopupHeightFromItemCount(int items_count) -{ - 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); -} - -bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) -{ - // 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; - - 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) - } + 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(); - return menu_is_open; + return value_changed; } -void ImGui::EndMenu() +bool ImGui::InputFloat(const char *label, float *v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags extra_flags) { - // 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(); - } + 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); +} - EndPopup(); +bool ImGui::InputInt(const char *label, int *v, int step, int step_fast, ImGuiInputTextFlags extra_flags) +{ + // 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); } -// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. -void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) +bool ImGui::InputFloatN(const char *label, float *v, int components, int decimal_precision, ImGuiInputTextFlags extra_flags) { - ImGuiContext& g = *GImGui; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - 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(); - } + 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(); - 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(); + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; } -static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) +bool ImGui::InputFloat2(const char *label, float v[2], int decimal_precision, ImGuiInputTextFlags extra_flags) { - 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); + return InputFloatN(label, v, 2, decimal_precision, extra_flags); } -// 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) +bool ImGui::InputFloat3(const char *label, float v[3], int decimal_precision, ImGuiInputTextFlags extra_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); - } + return InputFloatN(label, v, 3, decimal_precision, extra_flags); } -void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) +bool ImGui::InputFloat4(const char *label, float v[4], int decimal_precision, ImGuiInputTextFlags extra_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; + return InputFloatN(label, v, 4, decimal_precision, extra_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; - } +bool ImGui::InputIntN(const char *label, int *v, int components, ImGuiInputTextFlags extra_flags) +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; - // Tooltip - if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) - ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); + 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(); - return pressed; + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; } -bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) +bool ImGui::InputInt2(const char *label, int v[2], ImGuiInputTextFlags extra_flags) { - return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); + return InputIntN(label, v, 2, extra_flags); } -void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) +bool ImGui::InputInt3(const char *label, int v[3], ImGuiInputTextFlags extra_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; - } + return InputIntN(label, v, 3, extra_flags); +} - 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(); - } +bool ImGui::InputInt4(const char *label, int v[4], ImGuiInputTextFlags extra_flags) +{ + return InputIntN(label, v, 4, extra_flags); +} - g.ColorEditOptions = opts; - EndPopup(); +static float CalcMaxPopupHeightFromItemCount(int items_count) +{ + 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); +} + +bool ImGui::BeginCombo(const char *label, const char *preview_value, ImGuiComboFlags flags) +{ + // 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; + + 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; } -static void ColorPickerOptionsPopup(ImGuiColorEditFlags flags, const float* ref_col) +void ImGui::EndCombo() { - 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(); + const ImGuiStyle &style = GImGui->Style; + if (style.FramePadding.x != style.WindowPadding.x) + Unindent(style.FramePadding.x - style.WindowPadding.x); + 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(); - } +// 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; +} - 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(); - } - } +// 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; +} - if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) - { - SameLine(0, style.ItemInnerSpacing.x); - TextUnformatted(label, label_display_end); - } +// 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; +} - // 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]; - } - } +// 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; +} - PopID(); - EndGroup(); +// 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; - // 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(); - } + 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. - // 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; + BeginGroup(); + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - return value_changed; + BeginChildFrame(id, frame_bb.GetSize()); + return true; } -bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) +bool ImGui::ListBoxHeader(const char *label, int items_count, int height_in_items) { - 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; + // 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); } -// '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) +void ImGui::ListBoxFooter() { - 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 - } + 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; } -static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w) +bool ImGui::BeginMainMenuBar() { - 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); + 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; } -// 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; - } - } +void ImGui::EndMainMenuBar() +{ + EndMenuBar(); - // 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 + // 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); - if (!(flags & ImGuiColorEditFlags_NoSidePreview)) - { - SameLine(0, style.ItemInnerSpacing.x); - BeginGroup(); - } + End(); + PopStyleVar(2); +} - 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); - } - } +bool ImGui::BeginMenuBar() +{ + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return false; + if (!(window->Flags & ImGuiWindowFlags_MenuBar)) + return false; - 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(); - } + IM_ASSERT(!window->DC.MenuBarAppending); + BeginGroup(); // Save position + PushID("##menubar"); - // 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]); + // 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); - // 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(); - } + 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; +} - // 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]); - } - } +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; +} - 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)); +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(); + } - 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); - } + EndPopup(); +} - // 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); +// 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; - // 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); - } + 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); - EndGroup(); - PopID(); + const char *text_end = text ? FindRenderedTextEnd(text, NULL) : text; + if (text_end > text) + { + TextUnformatted(text, text_end); + Separator(); + } - return value_changed && memcmp(backup_initial_col, col, components * sizeof(float)); + 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(); } -// Horizontal separating line. -void ImGui::Separator() +static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) { - 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; - } + 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); +} - // Horizontal Separator - if (window->DC.ColumnsSet) - PopClipRect(); +// 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); + } +} - float x1 = window->Pos.x; - float x2 = window->Pos.x + window->Size.x; - if (!window->DC.GroupStack.empty()) - x1 += window->DC.IndentX; +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; +} - 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; - } +// 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; +} - window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x,bb.Min.y), GetColorU32(ImGuiCol_Separator)); +// '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); +} - if (g.LogEnabled) - LogRenderedText(NULL, IM_NEWLINE "--------------------------------"); +// 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)); +} - if (window->DC.ColumnsSet) - { - PushColumnClipRect(); - window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; - } +// 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; + 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; + 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(" |"); + 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 ImGui::SplitterBehavior(ImGuiID id, const ImRect& bb, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend) +bool ImGui::SplitterBehavior(ImGuiID id, const ImRect &bb, ImGuiAxis axis, float *size1, float *size2, float min_size1, float min_size2, float hover_extend) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; - 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; + 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 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 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(); - if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id)) - SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id)) + SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); - 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; - - // 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; - - // Apply resize - *size1 += mouse_delta; - *size2 -= mouse_delta; - bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); - } + 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; + + // 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; - // 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); + // Apply resize + *size1 += mouse_delta; + *size2 -= mouse_delta; + bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); + } - return held; + // 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); + + return held; } void ImGui::Spacing() { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - ItemSize(ImVec2(0,0)); + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; + ItemSize(ImVec2(0, 0)); } -void ImGui::Dummy(const ImVec2& size) +void ImGui::Dummy(const ImVec2 &size) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - ItemSize(bb); - ItemAdd(bb, 0); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb); + ItemAdd(bb, 0); } -bool ImGui::IsRectVisible(const ImVec2& size) +bool ImGui::IsRectVisible(const ImVec2 &size) { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); } -bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +bool ImGui::IsRectVisible(const ImVec2 &rect_min, const ImVec2 &rect_max) { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); + ImGuiWindow *window = GetCurrentWindowRead(); + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); } // 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() { - ImGuiWindow* window = GetCurrentWindow(); + 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.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; + 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; } void ImGui::EndGroup() { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext &g = *GImGui; + ImGuiWindow *window = GetCurrentWindow(); - IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls + IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls - ImGuiGroupData& group_data = window->DC.GroupStack.back(); + 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); + 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; + 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 (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; + // 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->DC.GroupStack.pop_back(); - //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] + // window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] } // Gets back to previous line and continue with horizontal layout @@ -12270,462 +12742,468 @@ void ImGui::EndGroup() // spacing_w >= 0 : enforce spacing amount void ImGui::SameLine(float pos_x, float spacing_w) { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - - 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; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; + + 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; } void ImGui::NewLine() { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems) + return; - 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; + 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; } void ImGui::NextColumn() { - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems || window->DC.ColumnsSet == NULL) - return; - - ImGuiContext& g = *GImGui; - PopItemWidth(); - PopClipRect(); - - 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; - - PushColumnClipRect(); - PushItemWidth(GetColumnWidth() * 0.65f); // FIXME: Move on columns setup + ImGuiWindow *window = GetCurrentWindow(); + if (window->SkipItems || window->DC.ColumnsSet == NULL) + return; + + ImGuiContext &g = *GImGui; + PopItemWidth(); + PopClipRect(); + + 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; + + PushColumnClipRect(); + PushItemWidth(GetColumnWidth() * 0.65f); // FIXME: Move on columns setup } int ImGui::GetColumnIndex() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.ColumnsSet ? window->DC.ColumnsSet->Current : 0; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.ColumnsSet ? window->DC.ColumnsSet->Current : 0; } int ImGui::GetColumnsCount() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.ColumnsSet ? window->DC.ColumnsSet->Count : 1; + ImGuiWindow *window = GetCurrentWindowRead(); + return window->DC.ColumnsSet ? window->DC.ColumnsSet->Count : 1; } -static float OffsetNormToPixels(const ImGuiColumnsSet* columns, float offset_norm) +static float OffsetNormToPixels(const ImGuiColumnsSet *columns, float offset_norm) { - return offset_norm * (columns->MaxX - columns->MinX); + return offset_norm * (columns->MaxX - columns->MinX); } -static float PixelsToOffsetNorm(const ImGuiColumnsSet* columns, float offset) +static float PixelsToOffsetNorm(const ImGuiColumnsSet *columns, float offset) { - return offset / (columns->MaxX - columns->MinX); + return offset / (columns->MaxX - columns->MinX); } -static inline float GetColumnsRectHalfWidth() { return 4.0f; } +static inline float GetColumnsRectHalfWidth() +{ + return 4.0f; +} -static float GetDraggedColumnOffset(ImGuiColumnsSet* columns, int column_index) +static float GetDraggedColumnOffset(ImGuiColumnsSet *columns, int column_index) { - // 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)); + // 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)); - 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); + 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); - return x; + return x; } float ImGui::GetColumnOffset(int column_index) { - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumnsSet* columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiWindow *window = GetCurrentWindowRead(); + ImGuiColumnsSet *columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); - /* - 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 (g.ActiveId) + { + ImGuiContext& g = *GImGui; + const ImGuiID column_id = columns->ColumnsSetId + ImGuiID(column_index); + if (g.ActiveId == column_id) + return GetDraggedColumnOffset(columns, column_index); + } + */ - const float t = columns->Columns[column_index].OffsetNorm; - const float x_offset = ImLerp(columns->MinX, columns->MaxX, t); - return x_offset; + const float t = columns->Columns[column_index].OffsetNorm; + const float x_offset = ImLerp(columns->MinX, columns->MaxX, t); + return x_offset; } -static float GetColumnWidthEx(ImGuiColumnsSet* columns, int column_index, bool before_resize = false) +static float GetColumnWidthEx(ImGuiColumnsSet *columns, int column_index, bool before_resize = false) { - if (column_index < 0) - column_index = columns->Current; + if (column_index < 0) + column_index = columns->Current; - 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); + 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); } float ImGui::GetColumnWidth(int column_index) { - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumnsSet* columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiWindow *window = GetCurrentWindowRead(); + ImGuiColumnsSet *columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); - if (column_index < 0) - column_index = columns->Current; - return OffsetNormToPixels(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); + if (column_index < 0) + column_index = columns->Current; + return OffsetNormToPixels(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); } void ImGui::SetColumnOffset(int column_index, float offset) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImGuiColumnsSet* columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiContext &g = *GImGui; + ImGuiWindow *window = g.CurrentWindow; + ImGuiColumnsSet *columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); - 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; + 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; - 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); + 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); - if (preserve_width) - SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); + if (preserve_width) + SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); } void ImGui::SetColumnWidth(int column_index, float width) { - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumnsSet* columns = window->DC.ColumnsSet; - IM_ASSERT(columns != NULL); + ImGuiWindow *window = GetCurrentWindowRead(); + ImGuiColumnsSet *columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); - if (column_index < 0) - column_index = columns->Current; - SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); + if (column_index < 0) + column_index = columns->Current; + SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); } void ImGui::PushColumnClipRect(int column_index) { - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumnsSet* columns = window->DC.ColumnsSet; - if (column_index < 0) - column_index = columns->Current; - - PushClipRect(columns->Columns[column_index].ClipRect.Min, columns->Columns[column_index].ClipRect.Max, false); -} - -static ImGuiColumnsSet* FindOrAddColumnsSet(ImGuiWindow* window, ImGuiID id) -{ - for (int n = 0; n < window->ColumnsStorage.Size; n++) - if (window->ColumnsStorage[n].ID == id) - return &window->ColumnsStorage[n]; - - 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); + ImGuiWindow *window = GetCurrentWindowRead(); + ImGuiColumnsSet *columns = window->DC.ColumnsSet; + if (column_index < 0) + column_index = columns->Current; + + PushClipRect(columns->Columns[column_index].ClipRect.Min, columns->Columns[column_index].ClipRect.Max, false); +} + +static ImGuiColumnsSet *FindOrAddColumnsSet(ImGuiWindow *window, ImGuiID id) +{ + for (int n = 0; n < window->ColumnsStorage.Size; n++) + if (window->ColumnsStorage[n].ID == id) + return &window->ColumnsStorage[n]; + + 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); + 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) +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); + 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); } void ImGui::Indent(float indent_w) { - 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; + 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; } void ImGui::Unindent(float indent_w) { - 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; + 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; } -void ImGui::TreePush(const char* str_id) +void ImGui::TreePush(const char *str_id) { - ImGuiWindow* window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - PushID(str_id ? str_id : "#TreePush"); + ImGuiWindow *window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(str_id ? str_id : "#TreePush"); } -void ImGui::TreePush(const void* ptr_id) +void ImGui::TreePush(const void *ptr_id) { - ImGuiWindow* window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - PushID(ptr_id ? ptr_id : (const void*)"#TreePush"); + ImGuiWindow *window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(ptr_id ? ptr_id : (const void *) "#TreePush"); } void ImGui::TreePushRawID(ImGuiID id) { - ImGuiWindow* window = GetCurrentWindow(); - Indent(); - window->DC.TreeDepth++; - window->IDStack.push_back(id); + ImGuiWindow *window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + window->IDStack.push_back(id); } void ImGui::TreePop() { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - Unindent(); + 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; + 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; - PopID(); + PopID(); } -void ImGui::Value(const char* prefix, bool b) +void ImGui::Value(const char *prefix, bool b) { - Text("%s: %s", prefix, (b ? "true" : "false")); + Text("%s: %s", prefix, (b ? "true" : "false")); } -void ImGui::Value(const char* prefix, int v) +void ImGui::Value(const char *prefix, int v) { - Text("%s: %d", prefix, v); + Text("%s: %d", prefix, v); } -void ImGui::Value(const char* prefix, unsigned int v) +void ImGui::Value(const char *prefix, unsigned int v) { - Text("%s: %d", prefix, v); + Text("%s: %d", prefix, v); } -void ImGui::Value(const char* prefix, float v, const char* float_format) +void ImGui::Value(const char *prefix, float v, const char *float_format) { - 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); - } + 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); + } } //----------------------------------------------------------------------------- @@ -12734,182 +13212,182 @@ void ImGui::Value(const char* prefix, float v, const char* float_format) void ImGui::ClearDragDrop() { - ImGuiContext& g = *GImGui; - g.DragDropActive = false; - g.DragDropPayload.Clear(); - g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; - g.DragDropAcceptIdCurrRectSurface = FLT_MAX; - g.DragDropAcceptFrameCount = -1; + ImGuiContext &g = *GImGui; + g.DragDropActive = false; + g.DragDropPayload.Clear(); + g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropAcceptFrameCount = -1; } -// Call when current ID is active. +// 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) { - 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; + 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; } void ImGui::EndDragDropSource() { - ImGuiContext& g = *GImGui; - IM_ASSERT(g.DragDropActive); + ImGuiContext &g = *GImGui; + IM_ASSERT(g.DragDropActive); - if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - EndTooltip(); - PopStyleColor(); - //PopStyleVar(); - } + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + EndTooltip(); + PopStyleColor(); + // PopStyleVar(); + } - // Discard the drag if have not called SetDragDropPayload() - if (g.DragDropPayload.DataFrameCount == -1) - ClearDragDrop(); + // Discard the drag if have not called SetDragDropPayload() + if (g.DragDropPayload.DataFrameCount == -1) + ClearDragDrop(); } // 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; +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; } // We don't use BeginDragDropTargetCustom() and duplicate its code because: @@ -12918,81 +13396,84 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) // 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; + ImGuiContext &g = *GImGui; + if (!g.DragDropActive) + return false; - ImGuiWindow* window = g.CurrentWindow; - if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) - return false; - if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) - return false; + ImGuiWindow *window = g.CurrentWindow; + if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) + return false; - 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; + 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; - g.DragDropTargetRect = display_rect; - g.DragDropTargetId = id; - return true; + g.DragDropTargetRect = display_rect; + g.DragDropTargetId = id; + 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; - 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; + 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() { - ImGuiContext& g = *GImGui; (void)g; - IM_ASSERT(g.DragDropActive); + ImGuiContext &g = *GImGui; + (void) g; + IM_ASSERT(g.DragDropActive); } //----------------------------------------------------------------------------- @@ -13000,82 +13481,82 @@ void ImGui::EndDragDropTarget() //----------------------------------------------------------------------------- #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 +# undef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# ifndef __MINGW32__ +# include +# else +# include +# endif #endif // 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(); +# 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(); } #else // Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers -static const char* GetClipboardTextFn_DefaultImpl(void*) +static const char *GetClipboardTextFn_DefaultImpl(void *) { - ImGuiContext& g = *GImGui; - return g.PrivateClipboard.empty() ? NULL : g.PrivateClipboard.begin(); + ImGuiContext &g = *GImGui; + return g.PrivateClipboard.empty() ? NULL : g.PrivateClipboard.begin(); } // Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers -static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +static void SetClipboardTextFn_DefaultImpl(void *, const char *text) { - 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 = *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; } #endif @@ -13083,28 +13564,29 @@ static void SetClipboardTextFn_DefaultImpl(void*, const char* text) // Win32 API IME support (for Asian languages, etc.) #if defined(_WIN32) && !defined(__GNUC__) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) -#include -#ifdef _MSC_VER -#pragma comment(lib, "imm32") -#endif +# include +# ifdef _MSC_VER +# pragma comment(lib, "imm32") +# endif static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y) { - // 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); - } + // 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); + } } #else -static void ImeSetInputScreenPosFn_DefaultImpl(int, int) {} +static void ImeSetInputScreenPosFn_DefaultImpl(int, int) +{} #endif @@ -13112,159 +13594,165 @@ static void ImeSetInputScreenPosFn_DefaultImpl(int, int) {} // HELP //----------------------------------------------------------------------------- -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(); +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(); } //----------------------------------------------------------------------------- @@ -13272,7 +13760,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) // 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" +# include "imgui_user.inl" #endif //----------------------------------------------------------------------------- diff --git a/attachments/simple_engine/imgui/imgui.h b/attachments/simple_engine/imgui/imgui.h index bd63a2b0..4b45b0f9 100644 --- a/attachments/simple_engine/imgui/imgui.h +++ b/attachments/simple_engine/imgui/imgui.h @@ -10,102 +10,102 @@ // User-editable configuration files (edit stock imconfig.h or define IMGUI_USER_CONFIG to your own filename) #ifdef IMGUI_USER_CONFIG -#include IMGUI_USER_CONFIG +# include IMGUI_USER_CONFIG #endif #if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_INCLUDE_IMCONFIG_H) -#include "imconfig.h" +# include "imconfig.h" #endif -#include // FLT_MAX -#include // va_list -#include // ptrdiff_t, NULL -#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp +#include // FLT_MAX +#include // va_list +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp -#define IMGUI_VERSION "1.60 WIP" +#define IMGUI_VERSION "1.60 WIP" // Define attributes of all API symbols declarations, e.g. for DLL under Windows. #ifndef IMGUI_API -#define IMGUI_API +# define IMGUI_API #endif // Define assertion handler. #ifndef IM_ASSERT -#include -#define IM_ASSERT(_EXPR) assert(_EXPR) +# include +# define IM_ASSERT(_EXPR) assert(_EXPR) #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))) +# 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) +# define IM_FMTARGS(FMT) +# define IM_FMTLIST(FMT) #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++. +#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++. #if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" #endif // 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) +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 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); +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData *data); #if defined(_MSC_VER) && !defined(__clang__) -typedef unsigned __int64 ImU64; // 64-bit unsigned integer +typedef unsigned __int64 ImU64; // 64-bit unsigned integer #else -typedef unsigned long long ImU64; // 64-bit unsigned integer -#endif +typedef unsigned long long ImU64; // 64-bit unsigned integer +#endif // Others helpers at bottom of the file: // class ImVector<> // Lightweight std::vector like class. @@ -113,22 +113,42 @@ typedef unsigned long long ImU64; // 64-bit unsigned integer 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; + 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 #endif }; 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; + 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 #endif }; @@ -136,577 +156,578 @@ struct ImVec4 // 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, 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 // Flags for ImGui::Begin() 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_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() }; // Flags for ImGui::InputText() 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() + 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() }; // 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_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) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiTreeNodeFlags_AllowOverlapMode = ImGuiTreeNodeFlags_AllowItemOverlap + , + ImGuiTreeNodeFlags_AllowOverlapMode = ImGuiTreeNodeFlags_AllowItemOverlap #endif }; // 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_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 }; // 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_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 }; // 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_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 }; // Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() 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_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 }; // 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. + // 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. +#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 + 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 @@ -715,98 +736,102 @@ enum ImGuiKey_ // 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_ + // 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. + 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. }; // 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) + 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 + //, 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 #endif }; @@ -815,92 +840,97 @@ enum ImGuiCol_ // 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_ { - // 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) + // 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 + , + ImGuiStyleVar_ChildWindowRounding = ImGuiStyleVar_ChildRounding #endif }; // Enumeration for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() enum ImGuiColorEditFlags_ { - 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() + 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() }; // Enumeration for GetMouseCursor() enum ImGuiMouseCursor_ { - 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_ + 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_ }; // 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_ { - 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) + 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) - // Obsolete names (will be removed) +// 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 + , + ImGuiSetCond_Always = ImGuiCond_Always, + ImGuiSetCond_Once = ImGuiCond_Once, + ImGuiSetCond_FirstUseEver = ImGuiCond_FirstUseEver, + ImGuiSetCond_Appearing = ImGuiCond_Appearing #endif }; @@ -908,155 +938,158 @@ enum ImGuiCond_ // 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 { - 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); + 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. + //------------------------------------------------------------------ + // 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. #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); + // [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); #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(); + //------------------------------------------------------------------ + // 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(); }; //----------------------------------------------------------------------------- @@ -1066,29 +1099,85 @@ struct ImGuiIO #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS namespace ImGui { - // OBSOLETED in 1.60 (from Dec 2017) - static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } - static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } - static inline ImVec2 CalcItemRectClosestPoint(const ImVec2& pos, bool on_edge = false, float outward = 0.f) { (void)on_edge; (void)outward; IM_ASSERT(0); return pos; } - // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) - static inline void ShowTestWindow() { return ShowDemoWindow(); } - static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } - static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } - static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } - static inline float GetItemsLineHeightWithSpacing() { 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() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } - static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } - static inline void SetNextWindowPosCenter(ImGuiCond c=0) { 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() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } - static inline bool IsPosHoveringAnyWindow(const ImVec2&) { 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() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } - static inline bool IsMouseHoveringWindow() { 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) { (void)str_id; (void)framed; ImGuiTreeNodeFlags default_open_flags = 1 << 5; return CollapsingHeader(label, (default_open ? default_open_flags : 0)); } +// OBSOLETED in 1.60 (from Dec 2017) +static inline bool IsAnyWindowFocused() +{ + return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); +} +static inline bool IsAnyWindowHovered() +{ + return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); +} +static inline ImVec2 CalcItemRectClosestPoint(const ImVec2 &pos, bool on_edge = false, float outward = 0.f) +{ + (void) on_edge; + (void) outward; + IM_ASSERT(0); + return pos; +} +// OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) +static inline void ShowTestWindow() +{ + return ShowDemoWindow(); +} +static inline bool IsRootWindowFocused() +{ + return IsWindowFocused(ImGuiFocusedFlags_RootWindow); +} +static inline bool IsRootWindowOrAnyChildFocused() +{ + return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); +} +static inline void SetNextWindowContentWidth(float w) +{ + SetNextWindowContentSize(ImVec2(w, 0.0f)); +} +static inline float GetItemsLineHeightWithSpacing() +{ + 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() +{ + return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); +} +static inline void AlignFirstTextHeightToWidgets() +{ + AlignTextToFramePadding(); +} +static inline void SetNextWindowPosCenter(ImGuiCond c = 0) +{ + 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() +{ + return IsItemHovered(ImGuiHoveredFlags_RectOnly); +} +static inline bool IsPosHoveringAnyWindow(const ImVec2 &) +{ + 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() +{ + return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); +} +static inline bool IsMouseHoveringWindow() +{ + 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) +{ + (void) str_id; + (void) framed; + ImGuiTreeNodeFlags default_open_flags = 1 << 5; + return CollapsingHeader(label, (default_open ? default_open_flags : 0)); +} +} // namespace ImGui #endif //----------------------------------------------------------------------------- @@ -1097,63 +1186,193 @@ namespace ImGui // 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 +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; } + 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. @@ -1163,68 +1382,143 @@ class ImVector // 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; } + 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) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Will obsolete +# define IMGUI_ONCE_UPON_A_FRAME \ + static ImGuiOnceUponAFrame imgui_oaf; \ + if (imgui_oaf) #endif // 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(); } + 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 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); + 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 // 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. +// 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) // 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). @@ -1232,123 +1526,166 @@ struct ImGuiTextBuffer // 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(); + 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; } + 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; + } }; // 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 { - 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. + 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. }; // 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; } + // 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 #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_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; + + ImColor() + { + Value.x = Value.y = Value.z = Value.w = 0.0f; + } + ImColor(int r, int g, int b, int a = 255) + { + float sc = 1.0f / 255.0f; + Value.x = (float) r * sc; + Value.y = (float) g * sc; + Value.z = (float) b * sc; + Value.w = (float) a * sc; + } + ImColor(ImU32 rgba) + { + float sc = 1.0f / 255.0f; + Value.x = (float) ((rgba >> 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); + } }; // 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. +// 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. // Usage: // ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced. @@ -1386,19 +1764,25 @@ struct ImColor // - 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 { - float StartPosY; - float ItemsHeight; - int ItemsCount, StepNo, DisplayStart, DisplayEnd; - - // 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. - - 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. + float StartPosY; + float ItemsHeight; + int ItemsCount, StepNo, DisplayStart, DisplayEnd; + + // 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. + + 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. }; //----------------------------------------------------------------------------- @@ -1410,18 +1794,25 @@ struct ImGuiListClipper // 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); +typedef void (*ImDrawCallback)(const ImDrawList *parent_list, const ImDrawCmd *cmd); // Typically, 1 command = 1 GPU draw call (unless command is a callback) 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; } + 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; + } }; // Vertex index (override with '#define ImDrawIdx unsigned int' inside in imconfig.h) @@ -1433,15 +1824,15 @@ typedef unsigned short ImDrawIdx; #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) -// 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. +// 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 @@ -1449,27 +1840,27 @@ IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; // You can also use them to simulate drawing layers and submit primitives in a different order than how they will be rendered. struct ImDrawChannel { - ImVector CmdBuffer; - ImVector IdxBuffer; + ImVector CmdBuffer; + ImVector IdxBuffer; }; enum ImDrawCornerFlags_ { - 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 + 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 }; enum ImDrawListFlags_ { - ImDrawListFlags_AntiAliasedLines = 1 << 0, - ImDrawListFlags_AntiAliasedFill = 1 << 1 + ImDrawListFlags_AntiAliasedLines = 1 << 0, + ImDrawListFlags_AntiAliasedFill = 1 << 1 }; // Draw command list @@ -1480,144 +1871,201 @@ enum ImDrawListFlags_ // 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& centre, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); - IMGUI_API void AddCircleFilled(const ImVec2& centre, 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& centre, float radius, float a_min, float a_max, int num_segments = 10); - IMGUI_API void PathArcToFast(const ImVec2& centre, 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(); + // 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 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. + 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. }; 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. - - // [Internal] - char Name[32]; // Name (strictly to ease debugging) - ImFont* DstFont; - - IMGUI_API 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. + + // [Internal] + char Name[32]; // Name (strictly to ease debugging) + ImFont *DstFont; + + IMGUI_API ImFontConfig(); }; struct ImFontGlyph { - 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 + 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 }; 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_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 }; // Load and rasterize multiple TTF/OTF fonts into a same texture. @@ -1630,158 +2078,199 @@ enum ImFontAtlasFlags_ // 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. 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_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 }; // 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 ~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. #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - typedef ImFontGlyph Glyph; // OBSOLETE 1.52+ + typedef ImFontGlyph Glyph; // OBSOLETE 1.52+ #endif }; #if defined(__clang__) -#pragma clang diagnostic pop +# pragma clang diagnostic pop #endif // Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) #ifdef IMGUI_INCLUDE_IMGUI_USER_H -#include "imgui_user.h" +# include "imgui_user.h" #endif diff --git a/attachments/simple_engine/imgui/imgui_draw.cpp b/attachments/simple_engine/imgui/imgui_draw.cpp index 9ba63056..4015ce09 100644 --- a/attachments/simple_engine/imgui/imgui_draw.cpp +++ b/attachments/simple_engine/imgui/imgui_draw.cpp @@ -10,61 +10,61 @@ // - Default font data #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) -#define _CRT_SECURE_NO_WARNINGS +# define _CRT_SECURE_NO_WARNINGS #endif #include "imgui.h" #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui_internal.h" -#include // vsnprintf, sscanf, printf +#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 +# 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 #endif #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 : 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 #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 +# 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 #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 "-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 #endif //------------------------------------------------------------------------- // STB libraries implementation //------------------------------------------------------------------------- -//#define IMGUI_STB_NAMESPACE ImGuiStb -//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION -//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +// #define IMGUI_STB_NAMESPACE ImGuiStb +// #define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +// #define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION #ifdef IMGUI_STB_NAMESPACE namespace IMGUI_STB_NAMESPACE @@ -72,54 +72,54 @@ 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 #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" +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wmissing-prototypes" +# pragma clang diagnostic ignored "-Wimplicit-fallthrough" #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] +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] #endif -#define STBRP_ASSERT(x) IM_ASSERT(x) +#define STBRP_ASSERT(x) IM_ASSERT(x) #ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION -#define STBRP_STATIC -#define STB_RECT_PACK_IMPLEMENTATION +# define STBRP_STATIC +# define STB_RECT_PACK_IMPLEMENTATION #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) +#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 +# define STBTT_STATIC +# define STB_TRUETYPE_IMPLEMENTATION #else -#define STBTT_DEF extern +# define STBTT_DEF extern #endif #include "stb_truetype.h" #ifdef __GNUC__ -#pragma GCC diagnostic pop +# pragma GCC diagnostic pop #endif #ifdef __clang__ -#pragma clang diagnostic pop +# pragma clang diagnostic pop #endif #ifdef _MSC_VER -#pragma warning (pop) +# pragma warning(pop) #endif #ifdef IMGUI_STB_NAMESPACE -} // namespace ImGuiStb +} // namespace ImGuiStb using namespace IMGUI_STB_NAMESPACE; #endif @@ -127,163 +127,163 @@ using namespace IMGUI_STB_NAMESPACE; // 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(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); +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); } // 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); +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); } //----------------------------------------------------------------------------- @@ -292,17 +292,17 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst) ImDrawListSharedData::ImDrawListSharedData() { - 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)); - } + 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)); + } } //----------------------------------------------------------------------------- @@ -311,109 +311,110 @@ ImDrawListSharedData::ImDrawListSharedData() 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. + 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(); + 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) +#define GetCurrentClipRect() (_ClipRectStack.Size ? _ClipRectStack.Data[_ClipRectStack.Size - 1] : _Data->ClipRectFullscreen) +#define GetCurrentTextureId() (_TextureIdStack.Size ? _TextureIdStack.Data[_TextureIdStack.Size - 1] : NULL) void ImDrawList::AddDrawCmd() { - ImDrawCmd draw_cmd; - draw_cmd.ClipRect = GetCurrentClipRect(); - draw_cmd.TextureId = GetCurrentTextureId(); + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = GetCurrentClipRect(); + draw_cmd.TextureId = GetCurrentTextureId(); - IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); - CmdBuffer.push_back(draw_cmd); + IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); + CmdBuffer.push_back(draw_cmd); } -void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) +void ImDrawList::AddCallback(ImDrawCallback callback, void *callback_data) { - 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) + 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) } // 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; + // 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; + // 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 @@ -422,769 +423,890 @@ void ImDrawList::UpdateTextureID() // 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(); + 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 ImDrawList::PushClipRectFullScreen() { - PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); + PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); } void ImDrawList::PopClipRect() { - IM_ASSERT(_ClipRectStack.Size > 0); - _ClipRectStack.pop_back(); - UpdateClipRect(); + IM_ASSERT(_ClipRectStack.Size > 0); + _ClipRectStack.pop_back(); + UpdateClipRect(); } void ImDrawList::PushTextureID(ImTextureID texture_id) { - _TextureIdStack.push_back(texture_id); - UpdateTextureID(); + _TextureIdStack.push_back(texture_id); + UpdateTextureID(); } void ImDrawList::PopTextureID() { - IM_ASSERT(_TextureIdStack.Size > 0); - _TextureIdStack.pop_back(); - UpdateTextureID(); + 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); - } - } + 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; + // 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; + 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) { - ImDrawCmd& draw_cmd = CmdBuffer.Data[CmdBuffer.Size-1]; - draw_cmd.ElemCount += idx_count; + 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 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; + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; } // 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; +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; } -// 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) +void ImDrawList::PrimRectUV(const ImVec2 &a, const ImVec2 &c, const ImVec2 &uv_a, const ImVec2 &uv_c, ImU32 col) { - 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& centre, 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)); - } + 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::PathArcTo(const ImVec2& centre, float radius, float a_min, float a_max, int num_segments) +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) { - 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)); - } + 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; } -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) +// 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) { - 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); - } + 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::PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +void ImDrawList::AddConvexPolyFilled(const ImVec2 *points, const int points_count, ImU32 col) { - 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); - } + 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::AddLine(const ImVec2& a, const ImVec2& b, ImU32 col, float thickness) +void ImDrawList::PathArcToFast(const ImVec2 ¢re, float radius, int a_min_of_12, int a_max_of_12) { - 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); + 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)); + } } -// 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) +void ImDrawList::PathArcTo(const ImVec2 ¢re, float radius, float a_min, float a_max, int num_segments) { - 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); + 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)); + } } -void ImDrawList::AddRectFilled(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, int rounding_corners_flags) +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) { - 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); - } + 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::AddRectFilledMultiColor(const ImVec2& a, const ImVec2& c, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +void ImDrawList::PathBezierCurveTo(const ImVec2 &p2, const ImVec2 &p3, const ImVec2 &p4, int num_segments) { - 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); + 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::AddQuad(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col, float thickness) +void ImDrawList::PathRect(const ImVec2 &a, const ImVec2 &b, float rounding, int rounding_corners) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathLineTo(d); - PathStroke(col, true, thickness); + 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::AddQuadFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, ImU32 col) +void ImDrawList::AddLine(const ImVec2 &a, const ImVec2 &b, ImU32 col, float thickness) { - if ((col & IM_COL32_A_MASK) == 0) - return; + 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); +} - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathLineTo(d); - PathFillConvex(col); +// 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::AddTriangle(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col, float thickness) +void ImDrawList::AddRectFilled(const ImVec2 &a, const ImVec2 &b, ImU32 col, float rounding, int rounding_corners_flags) { - if ((col & IM_COL32_A_MASK) == 0) - return; + 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); + } +} - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathStroke(col, true, thickness); +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) +{ + 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); } -void ImDrawList::AddTriangleFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col) +void ImDrawList::AddQuad(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col, float thickness) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathLineTo(d); + PathStroke(col, true, thickness); +} - PathLineTo(a); - PathLineTo(b); - PathLineTo(c); - PathFillConvex(col); +void ImDrawList::AddQuadFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, const ImVec2 &d, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathLineTo(d); + PathFillConvex(col); } -void ImDrawList::AddCircle(const ImVec2& centre, float radius, ImU32 col, int num_segments, float thickness) +void ImDrawList::AddTriangle(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col, float thickness) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - 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); + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathStroke(col, true, thickness); } -void ImDrawList::AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, int num_segments) +void ImDrawList::AddTriangleFilled(const ImVec2 &a, const ImVec2 &b, const ImVec2 &c, ImU32 col) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - 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); + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathFillConvex(col); } -void ImDrawList::AddBezierCurve(const ImVec2& pos0, const ImVec2& cp0, const ImVec2& cp1, const ImVec2& pos1, ImU32 col, float thickness, int num_segments) +void ImDrawList::AddCircle(const ImVec2 ¢re, float radius, ImU32 col, int num_segments, float thickness) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - PathLineTo(pos0); - PathBezierCurveTo(cp0, cp1, pos1, num_segments); - PathStroke(col, false, thickness); + 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); } -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 ImDrawList::AddCircleFilled(const ImVec2 ¢re, float radius, ImU32 col, int num_segments) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - if (text_end == NULL) - text_end = text_begin + strlen(text_begin); - if (text_begin == text_end) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - // Pull default font/size from the shared ImDrawListSharedData instance - if (font == NULL) - font = _Data->Font; - if (font_size == 0.0f) - font_size = _Data->FontSize; + 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); +} - IM_ASSERT(font->ContainerAtlas->TexID == _TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. +void ImDrawList::AddBezierCurve(const ImVec2 &pos0, const ImVec2 &cp0, const ImVec2 &cp1, const ImVec2 &pos1, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; - 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); + PathLineTo(pos0); + PathBezierCurveTo(cp0, cp1, pos1, num_segments); + PathStroke(col, false, thickness); } -void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +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) { - AddText(NULL, 0.0f, pos, col, text_begin, text_end); + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (text_end == NULL) + text_end = text_begin + strlen(text_begin); + if (text_begin == text_end) + return; + + // 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 == _TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change 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); } -void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col) +void ImDrawList::AddText(const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end) { - if ((col & IM_COL32_A_MASK) == 0) - return; - - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); - - PrimReserve(6, 4); - PrimRectUV(a, b, uv_a, uv_b, col); - - if (push_texture_id) - PopTextureID(); + AddText(NULL, 0.0f, pos, col, text_begin, text_end); } -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 ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2 &a, const ImVec2 &b, const ImVec2 &uv_a, const ImVec2 &uv_b, ImU32 col) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); - PrimReserve(6, 4); - PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); + PrimReserve(6, 4); + PrimRectUV(a, b, uv_a, uv_b, col); - if (push_texture_id) - PopTextureID(); + if (push_texture_id) + PopTextureID(); } -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 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) { - if ((col & IM_COL32_A_MASK) == 0) - return; + if ((col & IM_COL32_A_MASK) == 0) + return; - if (rounding <= 0.0f || (rounding_corners & ImDrawCornerFlags_All) == 0) - { - AddImage(user_texture_id, a, b, uv_a, uv_b, col); - return; - } + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); - if (push_texture_id) - PushTextureID(user_texture_id); + PrimReserve(6, 4); + PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); - 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); + if (push_texture_id) + PopTextureID(); +} - if (push_texture_id) - PopTextureID(); +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) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (rounding <= 0.0f || (rounding_corners & ImDrawCornerFlags_All) == 0) + { + AddImage(user_texture_id, a, b, uv_a, uv_b, col); + return; + } + + const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + if (push_texture_id) + PushTextureID(user_texture_id); + + 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); + + if (push_texture_id) + PopTextureID(); } //----------------------------------------------------------------------------- @@ -1194,34 +1316,34 @@ void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& a, c // 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; - } + 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) +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); - } - } + 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); + } + } } //----------------------------------------------------------------------------- @@ -1229,60 +1351,60 @@ void ImDrawData::ScaleClipRects(const ImVec2& scale) //----------------------------------------------------------------------------- // 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) +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); - } + 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) +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); - } + 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); + } } // 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) +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) { - const ImVec2 size = b - a; - const ImVec2 uv_size = uv_b - uv_a; - const ImVec2 scale = ImVec2( + 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); - - 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); - } + size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + + 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); + } } //----------------------------------------------------------------------------- @@ -1291,22 +1413,22 @@ void ImGui::ShadeVertsLinearUV(ImDrawVert* vert_start, ImDrawVert* vert_end, con ImFontConfig::ImFontConfig() { - 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; + 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; } //----------------------------------------------------------------------------- @@ -1315,817 +1437,2766 @@ ImFontConfig::ImFontConfig() // 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] = -{ - "..- -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 - " -}; +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] = + { + "..- -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] = -{ - // 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 + { + // 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 }; ImFontAtlas::ImFontAtlas() { - 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; + 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; } ImFontAtlas::~ImFontAtlas() { - Clear(); + Clear(); } -void ImFontAtlas::ClearInputData() +void ImFontAtlas::ClearInputData() { - 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; + 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; } -void ImFontAtlas::ClearTexData() +void ImFontAtlas::ClearTexData() { - if (TexPixelsAlpha8) - ImGui::MemFree(TexPixelsAlpha8); - if (TexPixelsRGBA32) - ImGui::MemFree(TexPixelsRGBA32); - TexPixelsAlpha8 = NULL; - TexPixelsRGBA32 = NULL; + if (TexPixelsAlpha8) + ImGui::MemFree(TexPixelsAlpha8); + if (TexPixelsRGBA32) + ImGui::MemFree(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; } -void ImFontAtlas::ClearFonts() +void ImFontAtlas::ClearFonts() { - for (int i = 0; i < Fonts.Size; i++) - IM_DELETE(Fonts[i]); - Fonts.clear(); + for (int i = 0; i < Fonts.Size; i++) + IM_DELETE(Fonts[i]); + Fonts.clear(); } -void ImFontAtlas::Clear() +void ImFontAtlas::Clear() { - ClearInputData(); - ClearTexData(); - ClearFonts(); + ClearInputData(); + ClearTexData(); + ClearFonts(); } -void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +void ImFontAtlas::GetTexDataAsAlpha8(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel) { - // Build atlas on demand - if (TexPixelsAlpha8 == NULL) - { - if (ConfigData.empty()) - AddFontDefault(); - 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; + // Build atlas on demand + if (TexPixelsAlpha8 == NULL) + { + if (ConfigData.empty()) + AddFontDefault(); + 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 ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +void ImFontAtlas::GetTexDataAsRGBA32(unsigned char **out_pixels, int *out_width, int *out_height, int *out_bytes_per_pixel) { - // 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++)); - } - } - - *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) -{ - 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()); // 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); - } + // 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++)); + } + } + + *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; +} - // Invalidate texture - ClearTexData(); - return new_font_cfg.DstFont; +ImFont *ImFontAtlas::AddFont(const ImFontConfig *font_cfg) +{ + 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()); // 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; } // 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) { return c >= '\\' ? c-36 : c-35; } -static void Decode85(const unsigned char* src, unsigned char* dst) +static const char *GetDefaultCompressedFontDataTTFBase85(); +static unsigned int Decode85Byte(char c) { - 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; - } + return c >= '\\' ? c - 36 : c - 35; +} +static void Decode85(const unsigned char *src, unsigned char *dst) +{ + 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; + } } // Load embedded ProggyClean.ttf at size 13, disable oversampling -ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) +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; + 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) +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); + 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); } // 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) +ImFont *ImFontAtlas::AddFontFromMemoryTTF(void *ttf_data, int ttf_size, float size_pixels, const ImFontConfig *font_cfg_template, const ImWchar *glyph_ranges) { - 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); + 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); } -ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +ImFont *ImFontAtlas::AddFontFromMemoryCompressedTTF(const void *compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig *font_cfg_template, const ImWchar *glyph_ranges) { - 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); - - 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); + 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); + + 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); } -ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) +ImFont *ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char *compressed_ttf_data_base85, float size_pixels, const ImFontConfig *font_cfg, const ImWchar *glyph_ranges) { - 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; + 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; } int ImFontAtlas::AddCustomRectRegular(unsigned int id, int width, int height) { - 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 + 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 } -int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) +int ImFontAtlas::AddCustomRectFontGlyph(ImFont *font, ImWchar id, int width, int height, float advance_x, const ImVec2 &offset) { - 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 + 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 } -void ImFontAtlas::CalcCustomRectUV(const CustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) +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); + 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]) +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; + 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() +bool ImFontAtlas::Build() { - return ImFontAtlasBuildWithStbTruetype(this); + return ImFontAtlasBuildWithStbTruetype(this); } -void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) +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); - } + 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) +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]]; + 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) +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; + 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) +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); + 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) +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++; + 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) +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); + 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); + } } -void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +static void ImFontAtlasBuildRenderDefaultTexData(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); - } + 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); +} - // Build all fonts lookup tables - for (int i = 0; i < atlas->Fonts.Size; i++) - atlas->Fonts[i]->BuildLookupTable(); +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(); } // Retrieve list of range (2 int per range, values are inclusive) -const ImWchar* ImFontAtlas::GetGlyphRangesDefault() +const ImWchar *ImFontAtlas::GetGlyphRangesDefault() { - static const ImWchar ranges[] = - { - 0x0020, 0x00FF, // Basic Latin + Latin Supplement - 0, - }; - return &ranges[0]; + static const ImWchar ranges[] = + { + 0x0020, + 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; } -const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +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]; + 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() +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]; + 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::GetGlyphRangesCyrillic() +const ImWchar *ImFontAtlas::GetGlyphRangesJapanese() { - 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]; + // 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::GetGlyphRangesThai() +const ImWchar *ImFontAtlas::GetGlyphRangesCyrillic() { - static const ImWchar ranges[] = - { - 0x0020, 0x00FF, // Basic Latin - 0x2010, 0x205E, // Punctuations - 0x0E00, 0x0E7F, // Thai - 0, - }; - return &ranges[0]; + 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]; } //----------------------------------------------------------------------------- // ImFontAtlas::GlyphRangesBuilder //----------------------------------------------------------------------------- -void ImFontAtlas::GlyphRangesBuilder::AddText(const char* text, const char* text_end) +void ImFontAtlas::GlyphRangesBuilder::AddText(const char *text, const char *text_end) { - 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); - } + 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); + } } -void ImFontAtlas::GlyphRangesBuilder::AddRanges(const ImWchar* ranges) +void ImFontAtlas::GlyphRangesBuilder::AddRanges(const ImWchar *ranges) { - for (; ranges[0]; ranges += 2) - for (ImWchar c = ranges[0]; c <= ranges[1]; c++) - AddChar(c); + for (; ranges[0]; ranges += 2) + for (ImWchar c = ranges[0]; c <= ranges[1]; c++) + AddChar(c); } -void ImFontAtlas::GlyphRangesBuilder::BuildRanges(ImVector* out_ranges) +void ImFontAtlas::GlyphRangesBuilder::BuildRanges(ImVector *out_ranges) { - 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); + 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); } //----------------------------------------------------------------------------- @@ -2134,519 +4205,563 @@ void ImFontAtlas::GlyphRangesBuilder::BuildRanges(ImVector* out_ranges) ImFont::ImFont() { - Scale = 1.0f; - FallbackChar = (ImWchar)'?'; - DisplayOffset = ImVec2(0.0f, 1.0f); - ClearOutputData(); + Scale = 1.0f; + FallbackChar = (ImWchar) '?'; + DisplayOffset = ImVec2(0.0f, 1.0f); + ClearOutputData(); } 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(); + // 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(); } -void ImFont::ClearOutputData() +void ImFont::ClearOutputData() { - 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; + 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; } void ImFont::BuildLookupTable() { - 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; + 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(); + FallbackChar = c; + BuildLookupTable(); } 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, (unsigned short)-1); + 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); } void ImFont::AddGlyph(ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_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); + 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); } void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) { - 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; + 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; + 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; + 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 +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; + 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 +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" + // 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; +} - 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 +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; +} - const char* word_end = text; - const char* prev_word_end = NULL; - bool inside_word = true; +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); + } +} - 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; +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; } //----------------------------------------------------------------------------- @@ -2655,70 +4770,72 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col static inline float ImAcos01(float x) { - 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. + 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. } // 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) +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); + 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); } //----------------------------------------------------------------------------- @@ -2731,113 +4848,152 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im static unsigned int stb_decompress_length(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__dout; -static void stb__match(unsigned char *data, unsigned int length) +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++; + // 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; + 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)) +#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; + 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; + 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; - } + 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; + } } //----------------------------------------------------------------------------- @@ -2849,7 +5005,7 @@ static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsi // File: 'ProggyClean.ttf' (41208 bytes) // Exported using binary_to_compressed_c.cpp //----------------------------------------------------------------------------- -static const char proggy_clean_ttf_compressed_data_base85[11980+1] = +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/" "2*>]b(MC;$jPfY.;h^`IWM9Qo#t'X#(v#Y9w0#1D$CIf;W'#pWUPXOuxXuU(H9M(1=Ke$$'5F%)]0^#0X@U.a // FILE* -#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf -#include // INT_MIN, INT_MAX +#include // INT_MIN, INT_MAX +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // FILE* #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) #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" +# 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" #endif //----------------------------------------------------------------------------- @@ -43,14 +43,14 @@ 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_ +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_ //------------------------------------------------------------------------- // STB libraries @@ -61,113 +61,282 @@ namespace ImGuiStb #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 +#define STB_TEXTEDIT_STRING ImGuiTextEditState +#define STB_TEXTEDIT_CHARTYPE ImWchar +#define STB_TEXTEDIT_GETWIDTH_NEWLINE -1.0f #include "stb_textedit.h" -} // namespace ImGuiStb +} // namespace ImGuiStb //----------------------------------------------------------------------------- // Context //----------------------------------------------------------------------------- #ifndef GImGui -extern IMGUI_API ImGuiContext* GImGui; // Current implicit ImGui context pointer +extern IMGUI_API ImGuiContext *GImGui; // Current implicit ImGui context pointer #endif //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- -#define IM_PI 3.14159265358979323846f +#define IM_PI 3.14159265358979323846f // 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 +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: 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; } +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: 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); +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: 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); +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) { 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+=(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 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 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 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+=(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 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 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); +} #endif -static inline int ImMin(int lhs, int rhs) { return lhs < rhs ? lhs : rhs; } -static inline int ImMax(int lhs, int rhs) { return lhs >= rhs ? lhs : rhs; } -static inline float ImMin(float lhs, float rhs) { return lhs < rhs ? lhs : rhs; } -static inline float ImMax(float lhs, float rhs) { return lhs >= rhs ? lhs : rhs; } -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 int ImClamp(int v, int mn, int mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } -static inline float ImClamp(float v, float mn, float mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } -static inline ImVec2 ImClamp(const ImVec2& f, const ImVec2& mn, ImVec2 mx) { return ImVec2(ImClamp(f.x,mn.x,mx.x), ImClamp(f.y,mn.y,mx.y)); } -static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } -static inline void ImSwap(int& a, int& b) { int tmp = a; a = b; b = tmp; } -static inline void ImSwap(float& a, float& b) { float tmp = a; a = b; b = tmp; } -static inline int ImLerp(int a, int b, float t) { return (int)(a + (b - a) * t); } -static inline float ImLerp(float a, float b, float t) { return a + (b - a) * t; } -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 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 1.0f / sqrtf(d); return fail_value; } -static inline float ImFloor(float f) { return (float)(int)f; } -static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2((float)(int)v.x, (float)(int)v.y); } -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 ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline int ImMin(int lhs, int rhs) +{ + return lhs < rhs ? lhs : rhs; +} +static inline int ImMax(int lhs, int rhs) +{ + return lhs >= rhs ? lhs : rhs; +} +static inline float ImMin(float lhs, float rhs) +{ + return lhs < rhs ? lhs : rhs; +} +static inline float ImMax(float lhs, float rhs) +{ + return lhs >= rhs ? lhs : rhs; +} +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 int ImClamp(int v, int mn, int mx) +{ + return (v < mn) ? mn : (v > mx) ? mx : + v; +} +static inline float ImClamp(float v, float mn, float mx) +{ + return (v < mn) ? mn : (v > mx) ? mx : + v; +} +static inline ImVec2 ImClamp(const ImVec2 &f, const ImVec2 &mn, ImVec2 mx) +{ + return ImVec2(ImClamp(f.x, mn.x, mx.x), ImClamp(f.y, mn.y, mx.y)); +} +static inline float ImSaturate(float f) +{ + return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : + f; +} +static inline void ImSwap(int &a, int &b) +{ + int tmp = a; + a = b; + b = tmp; +} +static inline void ImSwap(float &a, float &b) +{ + float tmp = a; + a = b; + b = tmp; +} +static inline int ImLerp(int a, int b, float t) +{ + return (int) (a + (b - a) * t); +} +static inline float ImLerp(float a, float b, float t) +{ + return a + (b - a) * t; +} +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 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 1.0f / sqrtf(d); + return fail_value; +} +static inline float ImFloor(float f) +{ + return (float) (int) f; +} +static inline ImVec2 ImFloor(const ImVec2 &v) +{ + return ImVec2((float) (int) v.x, (float) (int) v.y); +} +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 ImVec2 ImMul(const ImVec2 &lhs, const ImVec2 &rhs) +{ + return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); +} // 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) { 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; } } +struct ImNewPlacementDummy +{}; +inline void *operator new(size_t, ImNewPlacementDummy, void *ptr) +{ + 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; + } +} //----------------------------------------------------------------------------- // Types @@ -175,837 +344,1051 @@ template void IM_DELETE(T*& p) { if (p) { p->~T(); ImGui::MemFree(p enum ImGuiButtonFlags_ { - 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 + 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 }; enum ImGuiSliderFlags_ { - ImGuiSliderFlags_Vertical = 1 << 0 + ImGuiSliderFlags_Vertical = 1 << 0 }; 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. + // 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. }; enum ImGuiSelectableFlagsPrivate_ { - // 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 + // 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 }; 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 + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1 }; // Storage for LastItem data enum ImGuiItemStatusFlags_ { - ImGuiItemStatusFlags_HoveredRect = 1 << 0, - ImGuiItemStatusFlags_HasDisplayRect = 1 << 1 + ImGuiItemStatusFlags_HoveredRect = 1 << 0, + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1 }; // FIXME: this is in development, not exposed/functional as a generic feature yet. enum ImGuiLayoutType_ { - ImGuiLayoutType_Vertical, - ImGuiLayoutType_Horizontal + ImGuiLayoutType_Vertical, + ImGuiLayoutType_Horizontal }; enum ImGuiAxis { - ImGuiAxis_None = -1, - ImGuiAxis_X = 0, - ImGuiAxis_Y = 1 + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 }; enum ImGuiPlotType { - ImGuiPlotType_Lines, - ImGuiPlotType_Histogram + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram }; enum ImGuiDataType { - ImGuiDataType_Int, - ImGuiDataType_Float, - ImGuiDataType_Float2 + ImGuiDataType_Int, + ImGuiDataType_Float, + ImGuiDataType_Float2 }; enum ImGuiDir { - ImGuiDir_None = -1, - ImGuiDir_Left = 0, - ImGuiDir_Right = 1, - ImGuiDir_Up = 2, - ImGuiDir_Down = 3, - ImGuiDir_Count_ + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_Count_ }; enum ImGuiInputSource { - ImGuiInputSource_None = 0, - ImGuiInputSource_Mouse, - ImGuiInputSource_Nav, - ImGuiInputSource_NavKeyboard, // Only used occasionally for storage, not tested/handled by most code - ImGuiInputSource_NavGamepad, // " - ImGuiInputSource_Count_, + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, + ImGuiInputSource_Nav, + ImGuiInputSource_NavKeyboard, // Only used occasionally for storage, not tested/handled by most code + ImGuiInputSource_NavGamepad, // " + ImGuiInputSource_Count_, }; // FIXME-NAV: Clarify/expose various repeat delay/rate enum ImGuiInputReadMode { - ImGuiInputReadMode_Down, - ImGuiInputReadMode_Pressed, - ImGuiInputReadMode_Released, - ImGuiInputReadMode_Repeat, - ImGuiInputReadMode_RepeatSlow, - ImGuiInputReadMode_RepeatFast + ImGuiInputReadMode_Down, + ImGuiInputReadMode_Pressed, + ImGuiInputReadMode_Released, + ImGuiInputReadMode_Repeat, + ImGuiInputReadMode_RepeatSlow, + ImGuiInputReadMode_RepeatFast }; enum ImGuiNavHighlightFlags_ { - ImGuiNavHighlightFlags_TypeDefault = 1 << 0, - ImGuiNavHighlightFlags_TypeThin = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, - ImGuiNavHighlightFlags_NoRounding = 1 << 3 + ImGuiNavHighlightFlags_TypeDefault = 1 << 0, + ImGuiNavHighlightFlags_TypeThin = 1 << 1, + ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, + ImGuiNavHighlightFlags_NoRounding = 1 << 3 }; enum ImGuiNavDirSourceFlags_ { - ImGuiNavDirSourceFlags_Keyboard = 1 << 0, - ImGuiNavDirSourceFlags_PadDPad = 1 << 1, - ImGuiNavDirSourceFlags_PadLStick = 1 << 2 + ImGuiNavDirSourceFlags_Keyboard = 1 << 0, + ImGuiNavDirSourceFlags_PadDPad = 1 << 1, + ImGuiNavDirSourceFlags_PadLStick = 1 << 2 }; enum ImGuiNavForward { - ImGuiNavForward_None, - ImGuiNavForward_ForwardQueued, - ImGuiNavForward_ForwardActive + ImGuiNavForward_None, + ImGuiNavForward_ForwardQueued, + ImGuiNavForward_ForwardActive }; // 2D axis aligned bounding-box // NB: we can't rely on ImVec2 math operators being available here struct IMGUI_API ImRect { - 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; } + 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; + } }; // Stacked color modifier, backup of modified data so we can restore it struct ImGuiColMod { - ImGuiCol Col; - ImVec4 BackupValue; + ImGuiCol Col; + ImVec4 BackupValue; }; // Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. struct ImGuiStyleMod { - 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; } + 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; + 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 { - int Count; - float Spacing; - float Width, NextWidth; - float Pos[4], NextWidths[4]; - - ImGuiMenuColumns(); - void Update(int count, float spacing, bool clear); - float DeclColumns(float w0, float w1, float w2); - float CalcExtraSpace(float avail_w); + int Count; + float Spacing; + float Width, NextWidth; + float Pos[4], NextWidths[4]; + + ImGuiMenuColumns(); + void Update(int count, float spacing, bool clear); + float DeclColumns(float w0, float w1, float w2); + float CalcExtraSpace(float avail_w); }; // 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); + 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 ImGuiWindowSettings { - char* Name; - ImGuiID Id; - ImVec2 Pos; - ImVec2 Size; - bool Collapsed; - - ImGuiWindowSettings() { Name = NULL; Id = 0; Pos = Size = ImVec2(0,0); Collapsed = false; } + char *Name; + ImGuiID Id; + ImVec2 Pos; + ImVec2 Size; + bool Collapsed; + + ImGuiWindowSettings() + { + Name = NULL; + Id = 0; + Pos = Size = ImVec2(0, 0); + Collapsed = false; + } }; 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)); } + 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 + 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; } + 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(); - } + 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(); + } }; struct IMGUI_API ImDrawListSharedData { - 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() + 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() - // Const data - // FIXME: Bake rounded corners fill/borders in atlas - ImVec2 CircleVtx12[12]; + // Const data + // FIXME: Bake rounded corners fill/borders in atlas + ImVec2 CircleVtx12[12]; - ImDrawListSharedData(); + ImDrawListSharedData(); }; struct ImDrawDataBuilder { - 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(); + 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(); } + 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(); + } }; // Storage for SetNexWindow** functions struct ImGuiNextWindowData { - 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; - } + 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 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)); - } + 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 + 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] - ImVectorItemFlagsStack; - ImVector ItemWidthStack; - ImVector TextWrapPosStack; - ImVectorGroupStack; - 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; - } + 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 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()); } + 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. +// 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; } + 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; + } }; //----------------------------------------------------------------------------- @@ -1015,143 +1398,152 @@ struct ImGuiItemHoveredDataBackup namespace ImGui { - // 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() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } - inline ImGuiWindow* GetCurrentWindow() { 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 +// 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() +{ + ImGuiContext &g = *GImGui; + return g.CurrentWindow; +} +inline ImGuiWindow *GetCurrentWindow() +{ + 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); +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 +# pragma clang diagnostic pop #endif #ifdef _MSC_VER -#pragma warning (pop) +# pragma warning(pop) #endif diff --git a/attachments/simple_engine/imgui/stb_rect_pack.h b/attachments/simple_engine/imgui/stb_rect_pack.h index fafd8897..a9d82225 100644 --- a/attachments/simple_engine/imgui/stb_rect_pack.h +++ b/attachments/simple_engine/imgui/stb_rect_pack.h @@ -53,128 +53,126 @@ #ifndef STB_INCLUDE_STB_RECT_PACK_H #define STB_INCLUDE_STB_RECT_PACK_H -#define STB_RECT_PACK_VERSION 1 +#define STB_RECT_PACK_VERSION 1 #ifdef STBRP_STATIC -#define STBRP_DEF static +# define STBRP_DEF static #else -#define STBRP_DEF extern +# define STBRP_DEF extern #endif #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -typedef struct stbrp_context stbrp_context; -typedef struct stbrp_node stbrp_node; -typedef struct stbrp_rect stbrp_rect; + typedef struct stbrp_context stbrp_context; + typedef struct stbrp_node stbrp_node; + typedef struct stbrp_rect stbrp_rect; #ifdef STBRP_LARGE_RECTS -typedef int stbrp_coord; + typedef int stbrp_coord; #else typedef unsigned short stbrp_coord; #endif -STBRP_DEF void stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); -// Assign packed locations to rectangles. The rectangles are of type -// 'stbrp_rect' defined below, stored in the array 'rects', and there -// are 'num_rects' many of them. -// -// Rectangles which are successfully packed have the 'was_packed' flag -// set to a non-zero value and 'x' and 'y' store the minimum location -// on each axis (i.e. bottom-left in cartesian coordinates, top-left -// if you imagine y increasing downwards). Rectangles which do not fit -// have the 'was_packed' flag set to 0. -// -// You should not try to access the 'rects' array from another thread -// while this function is running, as the function temporarily reorders -// the array while it executes. -// -// To pack into another rectangle, you need to call stbrp_init_target -// again. To continue packing into the same rectangle, you can call -// this function again. Calling this multiple times with multiple rect -// arrays will probably produce worse packing results than calling it -// a single time with the full rectangle array, but the option is -// available. - -struct stbrp_rect -{ - // reserved for your use: - int id; - - // input: - stbrp_coord w, h; - - // output: - stbrp_coord x, y; - int was_packed; // non-zero if valid packing - -}; // 16 bytes, nominally - - -STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); -// Initialize a rectangle packer to: -// pack a rectangle that is 'width' by 'height' in dimensions -// using temporary storage provided by the array 'nodes', which is 'num_nodes' long -// -// You must call this function every time you start packing into a new target. -// -// There is no "shutdown" function. The 'nodes' memory must stay valid for -// the following stbrp_pack_rects() call (or calls), but can be freed after -// the call (or calls) finish. -// -// Note: to guarantee best results, either: -// 1. make sure 'num_nodes' >= 'width' -// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' -// -// If you don't do either of the above things, widths will be quantized to multiples -// of small integers to guarantee the algorithm doesn't run out of temporary storage. -// -// If you do #2, then the non-quantized algorithm will be used, but the algorithm -// may run out of temporary storage and be unable to pack some rectangles. - -STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); -// Optionally call this function after init but before doing any packing to -// change the handling of the out-of-temp-memory scenario, described above. -// If you call init again, this will be reset to the default (false). - - -STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); -// Optionally select which packing heuristic the library should use. Different -// heuristics will produce better/worse results for different data sets. -// If you call init again, this will be reset to the default. - -enum -{ - STBRP_HEURISTIC_Skyline_default=0, - STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, - STBRP_HEURISTIC_Skyline_BF_sortHeight -}; - - -////////////////////////////////////////////////////////////////////////////// -// -// the details of the following structures don't matter to you, but they must -// be visible so you can handle the memory allocations for them - -struct stbrp_node -{ - stbrp_coord x,y; - stbrp_node *next; -}; - -struct stbrp_context -{ - int width; - int height; - int align; - int init_mode; - int heuristic; - int num_nodes; - stbrp_node *active_head; - stbrp_node *free_head; - stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' -}; + STBRP_DEF void stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects); + // Assign packed locations to rectangles. The rectangles are of type + // 'stbrp_rect' defined below, stored in the array 'rects', and there + // are 'num_rects' many of them. + // + // Rectangles which are successfully packed have the 'was_packed' flag + // set to a non-zero value and 'x' and 'y' store the minimum location + // on each axis (i.e. bottom-left in cartesian coordinates, top-left + // if you imagine y increasing downwards). Rectangles which do not fit + // have the 'was_packed' flag set to 0. + // + // You should not try to access the 'rects' array from another thread + // while this function is running, as the function temporarily reorders + // the array while it executes. + // + // To pack into another rectangle, you need to call stbrp_init_target + // again. To continue packing into the same rectangle, you can call + // this function again. Calling this multiple times with multiple rect + // arrays will probably produce worse packing results than calling it + // a single time with the full rectangle array, but the option is + // available. + + struct stbrp_rect + { + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + + }; // 16 bytes, nominally + + STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); + // Initialize a rectangle packer to: + // pack a rectangle that is 'width' by 'height' in dimensions + // using temporary storage provided by the array 'nodes', which is 'num_nodes' long + // + // You must call this function every time you start packing into a new target. + // + // There is no "shutdown" function. The 'nodes' memory must stay valid for + // the following stbrp_pack_rects() call (or calls), but can be freed after + // the call (or calls) finish. + // + // Note: to guarantee best results, either: + // 1. make sure 'num_nodes' >= 'width' + // or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' + // + // If you don't do either of the above things, widths will be quantized to multiples + // of small integers to guarantee the algorithm doesn't run out of temporary storage. + // + // If you do #2, then the non-quantized algorithm will be used, but the algorithm + // may run out of temporary storage and be unable to pack some rectangles. + + STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem); + // Optionally call this function after init but before doing any packing to + // change the handling of the out-of-temp-memory scenario, described above. + // If you call init again, this will be reset to the default (false). + + STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic); + // Optionally select which packing heuristic the library should use. Different + // heuristics will produce better/worse results for different data sets. + // If you call init again, this will be reset to the default. + + enum + { + STBRP_HEURISTIC_Skyline_default = 0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight + }; + + ////////////////////////////////////////////////////////////////////////////// + // + // the details of the following structures don't matter to you, but they must + // be visible so you can handle the memory allocations for them + + struct stbrp_node + { + stbrp_coord x, y; + stbrp_node *next; + }; + + struct stbrp_context + { + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' + }; #ifdef __cplusplus } @@ -189,385 +187,420 @@ struct stbrp_context #ifdef STB_RECT_PACK_IMPLEMENTATION #ifndef STBRP_SORT -#include -#define STBRP_SORT qsort +# include +# define STBRP_SORT qsort #endif #ifndef STBRP_ASSERT -#include -#define STBRP_ASSERT assert +# include +# define STBRP_ASSERT assert #endif enum { - STBRP__INIT_skyline = 1 + STBRP__INIT_skyline = 1 }; STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) { - switch (context->init_mode) { - case STBRP__INIT_skyline: - STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); - context->heuristic = heuristic; - break; - default: - STBRP_ASSERT(0); - } + switch (context->init_mode) + { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } } STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) { - if (allow_out_of_mem) - // if it's ok to run out of memory, then don't bother aligning them; - // this gives better packing, but may fail due to OOM (even though - // the rectangles easily fit). @TODO a smarter approach would be to only - // quantize once we've hit OOM, then we could get rid of this parameter. - context->align = 1; - else { - // if it's not ok to run out of memory, then quantize the widths - // so that num_nodes is always enough nodes. - // - // I.e. num_nodes * align >= width - // align >= width / num_nodes - // align = ceil(width/num_nodes) - - context->align = (context->width + context->num_nodes-1) / context->num_nodes; - } + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else + { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes - 1) / context->num_nodes; + } } STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) { - int i; + int i; #ifndef STBRP_LARGE_RECTS - STBRP_ASSERT(width <= 0xffff && height <= 0xffff); + STBRP_ASSERT(width <= 0xffff && height <= 0xffff); #endif - for (i=0; i < num_nodes-1; ++i) - nodes[i].next = &nodes[i+1]; - nodes[i].next = NULL; - context->init_mode = STBRP__INIT_skyline; - context->heuristic = STBRP_HEURISTIC_Skyline_default; - context->free_head = &nodes[0]; - context->active_head = &context->extra[0]; - context->width = width; - context->height = height; - context->num_nodes = num_nodes; - stbrp_setup_allow_out_of_mem(context, 0); - - // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) - context->extra[0].x = 0; - context->extra[0].y = 0; - context->extra[0].next = &context->extra[1]; - context->extra[1].x = (stbrp_coord) width; + for (i = 0; i < num_nodes - 1; ++i) + nodes[i].next = &nodes[i + 1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; #ifdef STBRP_LARGE_RECTS - context->extra[1].y = (1<<30); + context->extra[1].y = (1 << 30); #else - context->extra[1].y = 65535; + context->extra[1].y = 65535; #endif - context->extra[1].next = NULL; + context->extra[1].next = NULL; } // find minimum y position if it starts at x1 static int stbrp__skyline_find_min_y(stbrp_context *, stbrp_node *first, int x0, int width, int *pwaste) { - //(void)c; - stbrp_node *node = first; - int x1 = x0 + width; - int min_y, visited_width, waste_area; - STBRP_ASSERT(first->x <= x0); + //(void)c; + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + STBRP_ASSERT(first->x <= x0); - #if 0 +#if 0 // skip in case we're past the node while (node->next->x <= x0) ++node; - #else - STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency - #endif - - STBRP_ASSERT(node->x <= x0); - - min_y = 0; - waste_area = 0; - visited_width = 0; - while (node->x < x1) { - if (node->y > min_y) { - // raise min_y higher. - // we've accounted for all waste up to min_y, - // but we'll now add more waste for everything we've visted - waste_area += visited_width * (node->y - min_y); - min_y = node->y; - // the first time through, visited_width might be reduced - if (node->x < x0) - visited_width += node->next->x - x0; - else - visited_width += node->next->x - node->x; - } else { - // add waste area - int under_width = node->next->x - node->x; - if (under_width + visited_width > width) - under_width = width - visited_width; - waste_area += under_width * (min_y - node->y); - visited_width += under_width; - } - node = node->next; - } - - *pwaste = waste_area; - return min_y; +#else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency +#endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) + { + if (node->y > min_y) + { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } + else + { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; } typedef struct { - int x,y; - stbrp_node **prev_link; + int x, y; + stbrp_node **prev_link; } stbrp__findresult; static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) { - int best_waste = (1<<30), best_x, best_y = (1 << 30); - stbrp__findresult fr; - stbrp_node **prev, *node, *tail, **best = NULL; - - // align to multiple of c->align - width = (width + c->align - 1); - width -= width % c->align; - STBRP_ASSERT(width % c->align == 0); - - node = c->active_head; - prev = &c->active_head; - while (node->x + width <= c->width) { - int y,waste; - y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); - if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL - // bottom left - if (y < best_y) { - best_y = y; - best = prev; - } - } else { - // best-fit - if (y + height <= c->height) { - // can only use it if it first vertically - if (y < best_y || (y == best_y && waste < best_waste)) { - best_y = y; - best_waste = waste; - best = prev; - } - } - } - prev = &node->next; - node = node->next; - } - - best_x = (best == NULL) ? 0 : (*best)->x; - - // if doing best-fit (BF), we also have to try aligning right edge to each node position - // - // e.g, if fitting - // - // ____________________ - // |____________________| - // - // into - // - // | | - // | ____________| - // |____________| - // - // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned - // - // This makes BF take about 2x the time - - if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { - tail = c->active_head; - node = c->active_head; - prev = &c->active_head; - // find first node that's admissible - while (tail->x < width) - tail = tail->next; - while (tail) { - int xpos = tail->x - width; - int y,waste; - STBRP_ASSERT(xpos >= 0); - // find the left position that matches this - while (node->next->x <= xpos) { - prev = &node->next; - node = node->next; - } - STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); - y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); - if (y + height < c->height) { - if (y <= best_y) { - if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { - best_x = xpos; - STBRP_ASSERT(y <= best_y); - best_y = y; - best_waste = waste; - best = prev; - } - } - } - tail = tail->next; - } - } - - fr.prev_link = best; - fr.x = best_x; - fr.y = best_y; - return fr; + int best_waste = (1 << 30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) + { + int y, waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) + { // actually just want to test BL + // bottom left + if (y < best_y) + { + best_y = y; + best = prev; + } + } + else + { + // best-fit + if (y + height <= c->height) + { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) + { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) + { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) + { + int xpos = tail->x - width; + int y, waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) + { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height < c->height) + { + if (y <= best_y) + { + if (y < best_y || waste < best_waste || (waste == best_waste && xpos < best_x)) + { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; } static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) { - // find best position according to heuristic - stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); - stbrp_node *node, *cur; - - // bail if: - // 1. it failed - // 2. the best node doesn't fit (we don't always check this) - // 3. we're out of memory - if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { - res.prev_link = NULL; - return res; - } - - // on success, create new node - node = context->free_head; - node->x = (stbrp_coord) res.x; - node->y = (stbrp_coord) (res.y + height); - - context->free_head = node->next; - - // insert the new node into the right starting point, and - // let 'cur' point to the remaining nodes needing to be - // stiched back in - - cur = *res.prev_link; - if (cur->x < res.x) { - // preserve the existing one, so start testing with the next one - stbrp_node *next = cur->next; - cur->next = node; - cur = next; - } else { - *res.prev_link = node; - } - - // from here, traverse cur and free the nodes, until we get to one - // that shouldn't be freed - while (cur->next && cur->next->x <= res.x + width) { - stbrp_node *next = cur->next; - // move the current node to the free list - cur->next = context->free_head; - context->free_head = cur; - cur = next; - } - - // stitch the list back in - node->next = cur; - - if (cur->x < res.x + width) - cur->x = (stbrp_coord) (res.x + width); + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) + { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) + { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } + else + { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) + { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); #ifdef _DEBUG - cur = context->active_head; - while (cur->x < context->width) { - STBRP_ASSERT(cur->x < cur->next->x); - cur = cur->next; - } - STBRP_ASSERT(cur->next == NULL); - - { - stbrp_node *L1 = NULL, *L2 = NULL; - int count=0; - cur = context->active_head; - while (cur) { - L1 = cur; - cur = cur->next; - ++count; - } - cur = context->free_head; - while (cur) { - L2 = cur; - cur = cur->next; - ++count; - } - STBRP_ASSERT(count == context->num_nodes+2); - } + cur = context->active_head; + while (cur->x < context->width) + { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + stbrp_node *L1 = NULL, *L2 = NULL; + int count = 0; + cur = context->active_head; + while (cur) + { + L1 = cur; + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) + { + L2 = cur; + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes + 2); + } #endif - return res; + return res; } static int rect_height_compare(const void *a, const void *b) { - stbrp_rect *p = (stbrp_rect *) a; - stbrp_rect *q = (stbrp_rect *) b; - if (p->h > q->h) - return -1; - if (p->h < q->h) - return 1; - return (p->w > q->w) ? -1 : (p->w < q->w); + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); } static int rect_width_compare(const void *a, const void *b) { - stbrp_rect *p = (stbrp_rect *) a; - stbrp_rect *q = (stbrp_rect *) b; - if (p->w > q->w) - return -1; - if (p->w < q->w) - return 1; - return (p->h > q->h) ? -1 : (p->h < q->h); + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + if (p->w > q->w) + return -1; + if (p->w < q->w) + return 1; + return (p->h > q->h) ? -1 : (p->h < q->h); } static int rect_original_order(const void *a, const void *b) { - stbrp_rect *p = (stbrp_rect *) a; - stbrp_rect *q = (stbrp_rect *) b; - return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); + stbrp_rect *p = (stbrp_rect *) a; + stbrp_rect *q = (stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); } #ifdef STBRP_LARGE_RECTS -#define STBRP__MAXVAL 0xffffffff +# define STBRP__MAXVAL 0xffffffff #else -#define STBRP__MAXVAL 0xffff +# define STBRP__MAXVAL 0xffff #endif STBRP_DEF void stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) { - int i; - - // we use the 'was_packed' field internally to allow sorting/unsorting - for (i=0; i < num_rects; ++i) { - rects[i].was_packed = i; - #ifndef STBRP_LARGE_RECTS - STBRP_ASSERT(rects[i].w <= 0xffff && rects[i].h <= 0xffff); - #endif - } - - // sort according to heuristic - STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); - - for (i=0; i < num_rects; ++i) { - if (rects[i].w == 0 || rects[i].h == 0) { - rects[i].x = rects[i].y = 0; // empty rect needs no space - } else { - stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); - if (fr.prev_link) { - rects[i].x = (stbrp_coord) fr.x; - rects[i].y = (stbrp_coord) fr.y; - } else { - rects[i].x = rects[i].y = STBRP__MAXVAL; - } - } - } - - // unsort - STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); - - // set was_packed flags - for (i=0; i < num_rects; ++i) - rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + int i; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i = 0; i < num_rects; ++i) + { + rects[i].was_packed = i; +#ifndef STBRP_LARGE_RECTS + STBRP_ASSERT(rects[i].w <= 0xffff && rects[i].h <= 0xffff); +#endif + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i = 0; i < num_rects; ++i) + { + if (rects[i].w == 0 || rects[i].h == 0) + { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } + else + { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) + { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } + else + { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags + for (i = 0; i < num_rects; ++i) + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); } #endif diff --git a/attachments/simple_engine/imgui/stb_textedit.h b/attachments/simple_engine/imgui/stb_textedit.h index 3c300325..8db3ec27 100644 --- a/attachments/simple_engine/imgui/stb_textedit.h +++ b/attachments/simple_engine/imgui/stb_textedit.h @@ -17,7 +17,7 @@ // texts, as its performance does not scale and it has limited undo). // // Non-trivial behaviors are modelled after Windows text controls. -// +// // // LICENSE // @@ -213,20 +213,20 @@ // call this with the mouse x,y on a mouse down; it will update the cursor // and reset the selection start/end to the cursor point. the x,y must // be relative to the text widget, with (0,0) being the top left. -// +// // drag: // call this with the mouse x,y on a mouse drag/up; it will update the // cursor and the selection end point -// +// // cut: // call this to delete the current selection; returns true if there was // one. you should FIRST copy the current selection to the system paste buffer. // (To copy, just copy the current selection out of the string yourself.) -// +// // paste: // call this to paste text at the current cursor point or over the current // selection if there is one. -// +// // key: // call this for keyboard inputs sent to the textfield. you can use it // for "key down" events or for "translated" key events. if you need to @@ -235,7 +235,7 @@ // various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit // set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is // clear. -// +// // When rendering, you can read the cursor position and selection state from // the STB_TexteditState. // @@ -257,7 +257,6 @@ // efficient, but it's not horrible on modern computers. But you wouldn't // want to edit million-line files with it. - //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //// @@ -278,71 +277,70 @@ // #ifndef STB_TEXTEDIT_UNDOSTATECOUNT -#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +# define STB_TEXTEDIT_UNDOSTATECOUNT 99 #endif #ifndef STB_TEXTEDIT_UNDOCHARCOUNT -#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +# define STB_TEXTEDIT_UNDOCHARCOUNT 999 #endif #ifndef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_CHARTYPE int +# define STB_TEXTEDIT_CHARTYPE int #endif #ifndef STB_TEXTEDIT_POSITIONTYPE -#define STB_TEXTEDIT_POSITIONTYPE int +# define STB_TEXTEDIT_POSITIONTYPE int #endif typedef struct { - // private data - STB_TEXTEDIT_POSITIONTYPE where; - short insert_length; - short delete_length; - short char_storage; + // private data + STB_TEXTEDIT_POSITIONTYPE where; + short insert_length; + short delete_length; + short char_storage; } StbUndoRecord; typedef struct { - // private data - StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT]; - STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; - short undo_point, redo_point; - short undo_char_point, redo_char_point; + // private data + StbUndoRecord undo_rec[STB_TEXTEDIT_UNDOSTATECOUNT]; + STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; + short undo_point, redo_point; + short undo_char_point, redo_char_point; } StbUndoState; typedef struct { - ///////////////////// - // - // public data - // - - int cursor; - // position of the text cursor within the string - - int select_start; // selection start point - int select_end; - // selection start and end point in characters; if equal, no selection. - // note that start may be less than or greater than end (e.g. when - // dragging the mouse, start is where the initial click was, and you - // can drag in either direction) - - unsigned char insert_mode; - // each textfield keeps its own insert mode state. to keep an app-wide - // insert mode, copy this value in/out of the app state - - ///////////////////// - // - // private data - // - unsigned char cursor_at_end_of_line; // not implemented yet - unsigned char initialized; - unsigned char has_preferred_x; - unsigned char single_line; - unsigned char padding1, padding2, padding3; - float preferred_x; // this determines where the cursor up/down tries to seek to along x - StbUndoState undostate; + ///////////////////// + // + // public data + // + + int cursor; + // position of the text cursor within the string + + int select_start; // selection start point + int select_end; + // selection start and end point in characters; if equal, no selection. + // note that start may be less than or greater than end (e.g. when + // dragging the mouse, start is where the initial click was, and you + // can drag in either direction) + + unsigned char insert_mode; + // each textfield keeps its own insert mode state. to keep an app-wide + // insert mode, copy this value in/out of the app state + + ///////////////////// + // + // private data + // + unsigned char cursor_at_end_of_line; // not implemented yet + unsigned char initialized; + unsigned char has_preferred_x; + unsigned char single_line; + unsigned char padding1, padding2, padding3; + float preferred_x; // this determines where the cursor up/down tries to seek to along x + StbUndoState undostate; } STB_TexteditState; - //////////////////////////////////////////////////////////////////////// // // StbTexteditRow @@ -353,13 +351,12 @@ typedef struct // result of layout query typedef struct { - float x0,x1; // starting x location, end x location (allows for align=right, etc) - float baseline_y_delta; // position of baseline relative to previous row's baseline - float ymin,ymax; // height of row above and below baseline - int num_chars; + float x0, x1; // starting x location, end x location (allows for align=right, etc) + float baseline_y_delta; // position of baseline relative to previous row's baseline + float ymin, ymax; // height of row above and below baseline + int num_chars; } StbTexteditRow; -#endif //INCLUDE_STB_TEXTEDIT_H - +#endif // INCLUDE_STB_TEXTEDIT_H //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -368,17 +365,15 @@ typedef struct //// //// - // implementation isn't include-guarded, since it might have indirectly // included just the "header" portion #ifdef STB_TEXTEDIT_IMPLEMENTATION #ifndef STB_TEXTEDIT_memmove -#include -#define STB_TEXTEDIT_memmove memmove +# include +# define STB_TEXTEDIT_memmove memmove #endif - ///////////////////////////////////////////////////////////////////////////// // // Mouse input handling @@ -387,80 +382,84 @@ typedef struct // traverse the layout to locate the nearest character to a display position static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) { - StbTexteditRow r; - int n = STB_TEXTEDIT_STRINGLEN(str); - float base_y = 0, prev_x; - int i=0, k; - - r.x0 = r.x1 = 0; - r.ymin = r.ymax = 0; - r.num_chars = 0; - - // search rows to find one that straddles 'y' - while (i < n) { - STB_TEXTEDIT_LAYOUTROW(&r, str, i); - if (r.num_chars <= 0) - return n; - - if (i==0 && y < base_y + r.ymin) - return 0; - - if (y < base_y + r.ymax) - break; - - i += r.num_chars; - base_y += r.baseline_y_delta; - } - - // below all text, return 'after' last character - if (i >= n) - return n; - - // check if it's before the beginning of the line - if (x < r.x0) - return i; - - // check if it's before the end of the line - if (x < r.x1) { - // search characters in row for one that straddles 'x' - k = i; - prev_x = r.x0; - for (i=0; i < r.num_chars; ++i) { - float w = STB_TEXTEDIT_GETWIDTH(str, k, i); - if (x < prev_x+w) { - if (x < prev_x+w/2) - return k+i; - else - return k+i+1; - } - prev_x += w; - } - // shouldn't happen, but if it does, fall through to end-of-line case - } - - // if the last character is a newline, return that. otherwise return 'after' the last character - if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE) - return i+r.num_chars-1; - else - return i+r.num_chars; + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + float base_y = 0, prev_x; + int i = 0, k; + + r.x0 = r.x1 = 0; + r.ymin = r.ymax = 0; + r.num_chars = 0; + + // search rows to find one that straddles 'y' + while (i < n) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + return n; + + if (i == 0 && y < base_y + r.ymin) + return 0; + + if (y < base_y + r.ymax) + break; + + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // below all text, return 'after' last character + if (i >= n) + return n; + + // check if it's before the beginning of the line + if (x < r.x0) + return i; + + // check if it's before the end of the line + if (x < r.x1) + { + // search characters in row for one that straddles 'x' + k = i; + prev_x = r.x0; + for (i = 0; i < r.num_chars; ++i) + { + float w = STB_TEXTEDIT_GETWIDTH(str, k, i); + if (x < prev_x + w) + { + if (x < prev_x + w / 2) + return k + i; + else + return k + i + 1; + } + prev_x += w; + } + // shouldn't happen, but if it does, fall through to end-of-line case + } + + // if the last character is a newline, return that. otherwise return 'after' the last character + if (STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) == STB_TEXTEDIT_NEWLINE) + return i + r.num_chars - 1; + else + return i + r.num_chars; } // API click: on mouse down, move the cursor to the clicked location, and reset the selection static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { - state->cursor = stb_text_locate_coord(str, x, y); - state->select_start = state->cursor; - state->select_end = state->cursor; - state->has_preferred_x = 0; + state->cursor = stb_text_locate_coord(str, x, y); + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; } // API drag: on mouse drag, move the cursor and selection endpoint to the clicked location static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { - int p = stb_text_locate_coord(str, x, y); - if (state->select_start == state->select_end) - state->select_start = state->cursor; - state->cursor = state->select_end = p; + int p = stb_text_locate_coord(str, x, y); + if (state->select_start == state->select_end) + state->select_start = state->cursor; + state->cursor = state->select_end = p; } ///////////////////////////////////////////////////////////////////////////// @@ -477,577 +476,619 @@ static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditStat typedef struct { - float x,y; // position of n'th character - float height; // height of line - int first_char, length; // first char of row, and length - int prev_first; // first char of previous row + float x, y; // position of n'th character + float height; // height of line + int first_char, length; // first char of row, and length + int prev_first; // first char of previous row } StbFindState; // find the x/y location of a character, and remember info about the previous row in // case we get a move-up event (for page up, we'll have to rescan) static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *str, int n, int single_line) { - StbTexteditRow r; - int prev_start = 0; - int z = STB_TEXTEDIT_STRINGLEN(str); - int i=0, first; - - if (n == z) { - // if it's at the end, then find the last line -- simpler than trying to - // explicitly handle this case in the regular code - if (single_line) { - STB_TEXTEDIT_LAYOUTROW(&r, str, 0); - find->y = 0; - find->first_char = 0; - find->length = z; - find->height = r.ymax - r.ymin; - find->x = r.x1; - } else { - find->y = 0; - find->x = 0; - find->height = 1; - while (i < z) { - STB_TEXTEDIT_LAYOUTROW(&r, str, i); - prev_start = i; - i += r.num_chars; - } - find->first_char = i; - find->length = 0; - find->prev_first = prev_start; - } - return; - } - - // search rows to find the one that straddles character n - find->y = 0; - - for(;;) { - STB_TEXTEDIT_LAYOUTROW(&r, str, i); - if (n < i + r.num_chars) - break; - prev_start = i; - i += r.num_chars; - find->y += r.baseline_y_delta; - } - - find->first_char = first = i; - find->length = r.num_chars; - find->height = r.ymax - r.ymin; - find->prev_first = prev_start; - - // now scan to find xpos - find->x = r.x0; - i = 0; - for (i=0; first+i < n; ++i) - find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); + StbTexteditRow r; + int prev_start = 0; + int z = STB_TEXTEDIT_STRINGLEN(str); + int i = 0, first; + + if (n == z) + { + // if it's at the end, then find the last line -- simpler than trying to + // explicitly handle this case in the regular code + if (single_line) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + find->y = 0; + find->first_char = 0; + find->length = z; + find->height = r.ymax - r.ymin; + find->x = r.x1; + } + else + { + find->y = 0; + find->x = 0; + find->height = 1; + while (i < z) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + prev_start = i; + i += r.num_chars; + } + find->first_char = i; + find->length = 0; + find->prev_first = prev_start; + } + return; + } + + // search rows to find the one that straddles character n + find->y = 0; + + for (;;) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (n < i + r.num_chars) + break; + prev_start = i; + i += r.num_chars; + find->y += r.baseline_y_delta; + } + + find->first_char = first = i; + find->length = r.num_chars; + find->height = r.ymax - r.ymin; + find->prev_first = prev_start; + + // now scan to find xpos + find->x = r.x0; + i = 0; + for (i = 0; first + i < n; ++i) + find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); } -#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) +#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) // make the selection/cursor state valid if client altered the string static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - int n = STB_TEXTEDIT_STRINGLEN(str); - if (STB_TEXT_HAS_SELECTION(state)) { - if (state->select_start > n) state->select_start = n; - if (state->select_end > n) state->select_end = n; - // if clamping forced them to be equal, move the cursor to match - if (state->select_start == state->select_end) - state->cursor = state->select_start; - } - if (state->cursor > n) state->cursor = n; + int n = STB_TEXTEDIT_STRINGLEN(str); + if (STB_TEXT_HAS_SELECTION(state)) + { + if (state->select_start > n) + state->select_start = n; + if (state->select_end > n) + state->select_end = n; + // if clamping forced them to be equal, move the cursor to match + if (state->select_start == state->select_end) + state->cursor = state->select_start; + } + if (state->cursor > n) + state->cursor = n; } // delete characters while updating undo static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) { - stb_text_makeundo_delete(str, state, where, len); - STB_TEXTEDIT_DELETECHARS(str, where, len); - state->has_preferred_x = 0; + stb_text_makeundo_delete(str, state, where, len); + STB_TEXTEDIT_DELETECHARS(str, where, len); + state->has_preferred_x = 0; } // delete the section static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - stb_textedit_clamp(str, state); - if (STB_TEXT_HAS_SELECTION(state)) { - if (state->select_start < state->select_end) { - stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start); - state->select_end = state->cursor = state->select_start; - } else { - stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end); - state->select_start = state->cursor = state->select_end; - } - state->has_preferred_x = 0; - } + stb_textedit_clamp(str, state); + if (STB_TEXT_HAS_SELECTION(state)) + { + if (state->select_start < state->select_end) + { + stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start); + state->select_end = state->cursor = state->select_start; + } + else + { + stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end); + state->select_start = state->cursor = state->select_end; + } + state->has_preferred_x = 0; + } } // canoncialize the selection so start <= end static void stb_textedit_sortselection(STB_TexteditState *state) { - if (state->select_end < state->select_start) { - int temp = state->select_end; - state->select_end = state->select_start; - state->select_start = temp; - } + if (state->select_end < state->select_start) + { + int temp = state->select_end; + state->select_end = state->select_start; + state->select_start = temp; + } } // move cursor to first character of selection static void stb_textedit_move_to_first(STB_TexteditState *state) { - if (STB_TEXT_HAS_SELECTION(state)) { - stb_textedit_sortselection(state); - state->cursor = state->select_start; - state->select_end = state->select_start; - state->has_preferred_x = 0; - } + if (STB_TEXT_HAS_SELECTION(state)) + { + stb_textedit_sortselection(state); + state->cursor = state->select_start; + state->select_end = state->select_start; + state->has_preferred_x = 0; + } } // move cursor to last character of selection static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - if (STB_TEXT_HAS_SELECTION(state)) { - stb_textedit_sortselection(state); - stb_textedit_clamp(str, state); - state->cursor = state->select_end; - state->select_start = state->select_end; - state->has_preferred_x = 0; - } + if (STB_TEXT_HAS_SELECTION(state)) + { + stb_textedit_sortselection(state); + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->select_start = state->select_end; + state->has_preferred_x = 0; + } } #ifdef STB_TEXTEDIT_IS_SPACE -static int is_word_boundary( STB_TEXTEDIT_STRING *_str, int _idx ) +static int is_word_boundary(STB_TEXTEDIT_STRING *_str, int _idx) { - return _idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(_str,_idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(_str, _idx) ) ) : 1; + return _idx > 0 ? (STB_TEXTEDIT_IS_SPACE(STB_TEXTEDIT_GETCHAR(_str, _idx - 1)) && !STB_TEXTEDIT_IS_SPACE(STB_TEXTEDIT_GETCHAR(_str, _idx))) : 1; } -#ifndef STB_TEXTEDIT_MOVEWORDLEFT -static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *_str, int c ) +# ifndef STB_TEXTEDIT_MOVEWORDLEFT +static int stb_textedit_move_to_word_previous(STB_TEXTEDIT_STRING *_str, int c) { - while( c >= 0 && !is_word_boundary( _str, c ) ) - --c; + while (c >= 0 && !is_word_boundary(_str, c)) + --c; - if( c < 0 ) - c = 0; + if (c < 0) + c = 0; - return c; + return c; } -#define STB_TEXTEDIT_MOVEWORDLEFT stb_textedit_move_to_word_previous -#endif +# define STB_TEXTEDIT_MOVEWORDLEFT stb_textedit_move_to_word_previous +# endif -#ifndef STB_TEXTEDIT_MOVEWORDRIGHT -static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *_str, int c ) +# ifndef STB_TEXTEDIT_MOVEWORDRIGHT +static int stb_textedit_move_to_word_next(STB_TEXTEDIT_STRING *_str, int c) { - const int len = STB_TEXTEDIT_STRINGLEN(_str); - while( c < len && !is_word_boundary( _str, c ) ) - ++c; + const int len = STB_TEXTEDIT_STRINGLEN(_str); + while (c < len && !is_word_boundary(_str, c)) + ++c; - if( c > len ) - c = len; + if (c > len) + c = len; - return c; + return c; } -#define STB_TEXTEDIT_MOVEWORDRIGHT stb_textedit_move_to_word_next -#endif +# define STB_TEXTEDIT_MOVEWORDRIGHT stb_textedit_move_to_word_next +# endif #endif // update selection and cursor to match each other static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) { - if (!STB_TEXT_HAS_SELECTION(state)) - state->select_start = state->select_end = state->cursor; - else - state->cursor = state->select_end; + if (!STB_TEXT_HAS_SELECTION(state)) + state->select_start = state->select_end = state->cursor; + else + state->cursor = state->select_end; } // API cut: delete selection static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - if (STB_TEXT_HAS_SELECTION(state)) { - stb_textedit_delete_selection(str,state); // implicity clamps - state->has_preferred_x = 0; - return 1; - } - return 0; + if (STB_TEXT_HAS_SELECTION(state)) + { + stb_textedit_delete_selection(str, state); // implicity clamps + state->has_preferred_x = 0; + return 1; + } + return 0; } // API paste: replace existing selection with passed-in text static int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE const *ctext, int len) { - STB_TEXTEDIT_CHARTYPE *text = (STB_TEXTEDIT_CHARTYPE *) ctext; - // if there's a selection, the paste should delete it - stb_textedit_clamp(str, state); - stb_textedit_delete_selection(str,state); - // try to insert the characters - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) { - stb_text_makeundo_insert(state, state->cursor, len); - state->cursor += len; - state->has_preferred_x = 0; - return 1; - } - // remove the undo since we didn't actually insert the characters - if (state->undostate.undo_point) - --state->undostate.undo_point; - return 0; + STB_TEXTEDIT_CHARTYPE *text = (STB_TEXTEDIT_CHARTYPE *) ctext; + // if there's a selection, the paste should delete it + stb_textedit_clamp(str, state); + stb_textedit_delete_selection(str, state); + // try to insert the characters + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) + { + stb_text_makeundo_insert(state, state->cursor, len); + state->cursor += len; + state->has_preferred_x = 0; + return 1; + } + // remove the undo since we didn't actually insert the characters + if (state->undostate.undo_point) + --state->undostate.undo_point; + return 0; } // API key: process a keyboard input static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int key) { retry: - switch (key) { - default: { - int c = STB_TEXTEDIT_KEYTOTEXT(key); - if (c > 0) { - STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; - - // can't add newline in single-line mode - if (c == '\n' && state->single_line) - break; - - if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { - stb_text_makeundo_replace(str, state, state->cursor, 1, 1); - STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - ++state->cursor; - state->has_preferred_x = 0; - } - } else { - stb_textedit_delete_selection(str,state); // implicity clamps - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - stb_text_makeundo_insert(state, state->cursor, 1); - ++state->cursor; - state->has_preferred_x = 0; - } - } - } - break; - } + switch (key) + { + default: + { + int c = STB_TEXTEDIT_KEYTOTEXT(key); + if (c > 0) + { + STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; + + // can't add newline in single-line mode + if (c == '\n' && state->single_line) + break; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) + { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) + { + ++state->cursor; + state->has_preferred_x = 0; + } + } + else + { + stb_textedit_delete_selection(str, state); // implicity clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) + { + stb_text_makeundo_insert(state, state->cursor, 1); + ++state->cursor; + state->has_preferred_x = 0; + } + } + } + break; + } #ifdef STB_TEXTEDIT_K_INSERT - case STB_TEXTEDIT_K_INSERT: - state->insert_mode = !state->insert_mode; - break; + case STB_TEXTEDIT_K_INSERT: + state->insert_mode = !state->insert_mode; + break; #endif - - case STB_TEXTEDIT_K_UNDO: - stb_text_undo(str, state); - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_REDO: - stb_text_redo(str, state); - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_LEFT: - // if currently there's a selection, move cursor to start of selection - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_first(state); - else - if (state->cursor > 0) - --state->cursor; - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_RIGHT: - // if currently there's a selection, move cursor to end of selection - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_last(str, state); - else - ++state->cursor; - stb_textedit_clamp(str, state); - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT: - stb_textedit_clamp(str, state); - stb_textedit_prep_selection_at_cursor(state); - // move selection left - if (state->select_end > 0) - --state->select_end; - state->cursor = state->select_end; - state->has_preferred_x = 0; - break; + + case STB_TEXTEDIT_K_UNDO: + stb_text_undo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_REDO: + stb_text_redo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT: + // if currently there's a selection, move cursor to start of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else if (state->cursor > 0) + --state->cursor; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_RIGHT: + // if currently there's a selection, move cursor to end of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else + ++state->cursor; + stb_textedit_clamp(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + // move selection left + if (state->select_end > 0) + --state->select_end; + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; #ifdef STB_TEXTEDIT_MOVEWORDLEFT - case STB_TEXTEDIT_K_WORDLEFT: - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_first(state); - else { - state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor-1); - stb_textedit_clamp( str, state ); - } - break; - - case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT: - if( !STB_TEXT_HAS_SELECTION( state ) ) - stb_textedit_prep_selection_at_cursor(state); - - state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor-1); - state->select_end = state->cursor; - - stb_textedit_clamp( str, state ); - break; + case STB_TEXTEDIT_K_WORDLEFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else + { + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor - 1); + stb_textedit_clamp(str, state); + } + break; + + case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT: + if (!STB_TEXT_HAS_SELECTION(state)) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor - 1); + state->select_end = state->cursor; + + stb_textedit_clamp(str, state); + break; #endif #ifdef STB_TEXTEDIT_MOVEWORDRIGHT - case STB_TEXTEDIT_K_WORDRIGHT: - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_last(str, state); - else { - state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor+1); - stb_textedit_clamp( str, state ); - } - break; - - case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT: - if( !STB_TEXT_HAS_SELECTION( state ) ) - stb_textedit_prep_selection_at_cursor(state); - - state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor+1); - state->select_end = state->cursor; - - stb_textedit_clamp( str, state ); - break; + case STB_TEXTEDIT_K_WORDRIGHT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else + { + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor + 1); + stb_textedit_clamp(str, state); + } + break; + + case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT: + if (!STB_TEXT_HAS_SELECTION(state)) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor + 1); + state->select_end = state->cursor; + + stb_textedit_clamp(str, state); + break; #endif - case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: - stb_textedit_prep_selection_at_cursor(state); - // move selection right - ++state->select_end; - stb_textedit_clamp(str, state); - state->cursor = state->select_end; - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_DOWN: - case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: { - StbFindState find; - StbTexteditRow row; - int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; - - if (state->single_line) { - // on windows, up&down in single-line behave like left&right - key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); - goto retry; - } - - if (sel) - stb_textedit_prep_selection_at_cursor(state); - else if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_last(str,state); - - // compute current position of cursor point - stb_textedit_clamp(str, state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - - // now find character position down a row - if (find.length) { - float goal_x = state->has_preferred_x ? state->preferred_x : find.x; - float x; - int start = find.first_char + find.length; - state->cursor = start; - STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); - x = row.x0; - for (i=0; i < row.num_chars; ++i) { - float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) - break; - #endif - x += dx; - if (x > goal_x) - break; - ++state->cursor; - } - stb_textedit_clamp(str, state); - - state->has_preferred_x = 1; - state->preferred_x = goal_x; - - if (sel) - state->select_end = state->cursor; - } - break; - } - - case STB_TEXTEDIT_K_UP: - case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: { - StbFindState find; - StbTexteditRow row; - int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; - - if (state->single_line) { - // on windows, up&down become left&right - key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); - goto retry; - } - - if (sel) - stb_textedit_prep_selection_at_cursor(state); - else if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_first(state); - - // compute current position of cursor point - stb_textedit_clamp(str, state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - - // can only go up if there's a previous row - if (find.prev_first != find.first_char) { - // now find character position up a row - float goal_x = state->has_preferred_x ? state->preferred_x : find.x; - float x; - state->cursor = find.prev_first; - STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); - x = row.x0; - for (i=0; i < row.num_chars; ++i) { - float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) - break; - #endif - x += dx; - if (x > goal_x) - break; - ++state->cursor; - } - stb_textedit_clamp(str, state); - - state->has_preferred_x = 1; - state->preferred_x = goal_x; - - if (sel) - state->select_end = state->cursor; - } - break; - } - - case STB_TEXTEDIT_K_DELETE: - case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT: - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_delete_selection(str, state); - else { - int n = STB_TEXTEDIT_STRINGLEN(str); - if (state->cursor < n) - stb_textedit_delete(str, state, state->cursor, 1); - } - state->has_preferred_x = 0; - break; - - case STB_TEXTEDIT_K_BACKSPACE: - case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT: - if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_delete_selection(str, state); - else { - stb_textedit_clamp(str, state); - if (state->cursor > 0) { - stb_textedit_delete(str, state, state->cursor-1, 1); - --state->cursor; - } - } - state->has_preferred_x = 0; - break; - + case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + // move selection right + ++state->select_end; + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_DOWN: + case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: + { + StbFindState find; + StbTexteditRow row; + int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + + if (state->single_line) + { + // on windows, up&down in single-line behave like left&right + key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + // now find character position down a row + if (find.length) + { + float goal_x = state->has_preferred_x ? state->preferred_x : find.x; + float x; + int start = find.first_char + find.length; + state->cursor = start; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i = 0; i < row.num_chars; ++i) + { + float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); +#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; +#endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + } + break; + } + + case STB_TEXTEDIT_K_UP: + case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: + { + StbFindState find; + StbTexteditRow row; + int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + + if (state->single_line) + { + // on windows, up&down become left&right + key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + // can only go up if there's a previous row + if (find.prev_first != find.first_char) + { + // now find character position up a row + float goal_x = state->has_preferred_x ? state->preferred_x : find.x; + float x; + state->cursor = find.prev_first; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i = 0; i < row.num_chars; ++i) + { + float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); +#ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + break; +#endif + x += dx; + if (x > goal_x) + break; + ++state->cursor; + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + } + break; + } + + case STB_TEXTEDIT_K_DELETE: + case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else + { + int n = STB_TEXTEDIT_STRINGLEN(str); + if (state->cursor < n) + stb_textedit_delete(str, state, state->cursor, 1); + } + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_BACKSPACE: + case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else + { + stb_textedit_clamp(str, state); + if (state->cursor > 0) + { + stb_textedit_delete(str, state, state->cursor - 1, 1); + --state->cursor; + } + } + state->has_preferred_x = 0; + break; + #ifdef STB_TEXTEDIT_K_TEXTSTART2 - case STB_TEXTEDIT_K_TEXTSTART2: + case STB_TEXTEDIT_K_TEXTSTART2: #endif - case STB_TEXTEDIT_K_TEXTSTART: - state->cursor = state->select_start = state->select_end = 0; - state->has_preferred_x = 0; - break; + case STB_TEXTEDIT_K_TEXTSTART: + state->cursor = state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; #ifdef STB_TEXTEDIT_K_TEXTEND2 - case STB_TEXTEDIT_K_TEXTEND2: + case STB_TEXTEDIT_K_TEXTEND2: #endif - case STB_TEXTEDIT_K_TEXTEND: - state->cursor = STB_TEXTEDIT_STRINGLEN(str); - state->select_start = state->select_end = 0; - state->has_preferred_x = 0; - break; - + case STB_TEXTEDIT_K_TEXTEND: + state->cursor = STB_TEXTEDIT_STRINGLEN(str); + state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + #ifdef STB_TEXTEDIT_K_TEXTSTART2 - case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: #endif - case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT: - stb_textedit_prep_selection_at_cursor(state); - state->cursor = state->select_end = 0; - state->has_preferred_x = 0; - break; + case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = 0; + state->has_preferred_x = 0; + break; #ifdef STB_TEXTEDIT_K_TEXTEND2 - case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT: #endif - case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT: - stb_textedit_prep_selection_at_cursor(state); - state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str); - state->has_preferred_x = 0; - break; - + case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str); + state->has_preferred_x = 0; + break; #ifdef STB_TEXTEDIT_K_LINESTART2 - case STB_TEXTEDIT_K_LINESTART2: + case STB_TEXTEDIT_K_LINESTART2: #endif - case STB_TEXTEDIT_K_LINESTART: { - StbFindState find; - stb_textedit_clamp(str, state); - stb_textedit_move_to_first(state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - state->cursor = find.first_char; - state->has_preferred_x = 0; - break; - } + case STB_TEXTEDIT_K_LINESTART: + { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->cursor = find.first_char; + state->has_preferred_x = 0; + break; + } #ifdef STB_TEXTEDIT_K_LINEEND2 - case STB_TEXTEDIT_K_LINEEND2: + case STB_TEXTEDIT_K_LINEEND2: #endif - case STB_TEXTEDIT_K_LINEEND: { - StbFindState find; - stb_textedit_clamp(str, state); - stb_textedit_move_to_first(state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - - state->has_preferred_x = 0; - state->cursor = find.first_char + find.length; - if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) == STB_TEXTEDIT_NEWLINE) - --state->cursor; - break; - } + case STB_TEXTEDIT_K_LINEEND: + { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + state->has_preferred_x = 0; + state->cursor = find.first_char + find.length; + if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor - 1) == STB_TEXTEDIT_NEWLINE) + --state->cursor; + break; + } #ifdef STB_TEXTEDIT_K_LINESTART2 - case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT: #endif - case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: { - StbFindState find; - stb_textedit_clamp(str, state); - stb_textedit_prep_selection_at_cursor(state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - state->cursor = state->select_end = find.first_char; - state->has_preferred_x = 0; - break; - } + case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: + { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->cursor = state->select_end = find.first_char; + state->has_preferred_x = 0; + break; + } #ifdef STB_TEXTEDIT_K_LINEEND2 - case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: #endif - case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: { - StbFindState find; - stb_textedit_clamp(str, state); - stb_textedit_prep_selection_at_cursor(state); - stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - state->has_preferred_x = 0; - state->cursor = find.first_char + find.length; - if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) == STB_TEXTEDIT_NEWLINE) - --state->cursor; - state->select_end = state->cursor; - break; - } - -// @TODO: -// STB_TEXTEDIT_K_PGUP - move cursor up a page -// STB_TEXTEDIT_K_PGDOWN - move cursor down a page - } + case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: + { + StbFindState find; + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + state->has_preferred_x = 0; + state->cursor = find.first_char + find.length; + if (find.length > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor - 1) == STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->select_end = state->cursor; + break; + } + + // @TODO: + // STB_TEXTEDIT_K_PGUP - move cursor up a page + // STB_TEXTEDIT_K_PGDOWN - move cursor down a page + } } ///////////////////////////////////////////////////////////////////////////// @@ -1058,27 +1099,29 @@ static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, static void stb_textedit_flush_redo(StbUndoState *state) { - state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; } // discard the oldest entry in the undo list static void stb_textedit_discard_undo(StbUndoState *state) { - if (state->undo_point > 0) { - // if the 0th undo state has characters, clean those up - if (state->undo_rec[0].char_storage >= 0) { - int n = state->undo_rec[0].insert_length, i; - // delete n characters from all other records - state->undo_char_point = state->undo_char_point - (short) n; // vsnet05 - STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) ((size_t)state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE))); - for (i=0; i < state->undo_point; ++i) - if (state->undo_rec[i].char_storage >= 0) - state->undo_rec[i].char_storage = state->undo_rec[i].char_storage - (short) n; // vsnet05 // @OPTIMIZE: get rid of char_storage and infer it - } - --state->undo_point; - STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) ((size_t)state->undo_point*sizeof(state->undo_rec[0]))); - } + if (state->undo_point > 0) + { + // if the 0th undo state has characters, clean those up + if (state->undo_rec[0].char_storage >= 0) + { + int n = state->undo_rec[0].insert_length, i; + // delete n characters from all other records + state->undo_char_point = state->undo_char_point - (short) n; // vsnet05 + STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) ((size_t) state->undo_char_point * sizeof(STB_TEXTEDIT_CHARTYPE))); + for (i = 0; i < state->undo_point; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage = state->undo_rec[i].char_storage - (short) n; // vsnet05 // @OPTIMIZE: get rid of char_storage and infer it + } + --state->undo_point; + STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec + 1, (size_t) ((size_t) state->undo_point * sizeof(state->undo_rec[0]))); + } } // discard the oldest entry in the redo list--it's bad if this @@ -1087,231 +1130,250 @@ static void stb_textedit_discard_undo(StbUndoState *state) // fill up even though the undo buffer didn't static void stb_textedit_discard_redo(StbUndoState *state) { - int k = STB_TEXTEDIT_UNDOSTATECOUNT-1; - - if (state->redo_point <= k) { - // if the k'th undo state has characters, clean those up - if (state->undo_rec[k].char_storage >= 0) { - int n = state->undo_rec[k].insert_length, i; - // delete n characters from all other records - state->redo_char_point = state->redo_char_point + (short) n; // vsnet05 - STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((size_t)(STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE))); - for (i=state->redo_point; i < k; ++i) - if (state->undo_rec[i].char_storage >= 0) - state->undo_rec[i].char_storage = state->undo_rec[i].char_storage + (short) n; // vsnet05 - } - ++state->redo_point; - STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point-1, state->undo_rec + state->redo_point, (size_t) ((size_t)(STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point)*sizeof(state->undo_rec[0]))); - } + int k = STB_TEXTEDIT_UNDOSTATECOUNT - 1; + + if (state->redo_point <= k) + { + // if the k'th undo state has characters, clean those up + if (state->undo_rec[k].char_storage >= 0) + { + int n = state->undo_rec[k].insert_length, i; + // delete n characters from all other records + state->redo_char_point = state->redo_char_point + (short) n; // vsnet05 + STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point - n, (size_t) ((size_t) (STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_char_point) * sizeof(STB_TEXTEDIT_CHARTYPE))); + for (i = state->redo_point; i < k; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage = state->undo_rec[i].char_storage + (short) n; // vsnet05 + } + ++state->redo_point; + STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point - 1, state->undo_rec + state->redo_point, (size_t) ((size_t) (STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point) * sizeof(state->undo_rec[0]))); + } } static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numchars) { - // any time we create a new undo record, we discard redo - stb_textedit_flush_redo(state); - - // if we have no free records, we have to make room, by sliding the - // existing records down - if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) - stb_textedit_discard_undo(state); - - // if the characters to store won't possibly fit in the buffer, we can't undo - if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) { - state->undo_point = 0; - state->undo_char_point = 0; - return NULL; - } - - // if we don't have enough free characters in the buffer, we have to make room - while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) - stb_textedit_discard_undo(state); - - return &state->undo_rec[state->undo_point++]; + // any time we create a new undo record, we discard redo + stb_textedit_flush_redo(state); + + // if we have no free records, we have to make room, by sliding the + // existing records down + if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + stb_textedit_discard_undo(state); + + // if the characters to store won't possibly fit in the buffer, we can't undo + if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + { + state->undo_point = 0; + state->undo_char_point = 0; + return NULL; + } + + // if we don't have enough free characters in the buffer, we have to make room + while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + stb_textedit_discard_undo(state); + + return &state->undo_rec[state->undo_point++]; } static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) { - StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); - if (r == NULL) - return NULL; - - r->where = pos; - r->insert_length = (short) insert_len; - r->delete_length = (short) delete_len; - - if (insert_len == 0) { - r->char_storage = -1; - return NULL; - } else { - r->char_storage = state->undo_char_point; - state->undo_char_point = state->undo_char_point + (short) insert_len; - return &state->undo_char[r->char_storage]; - } + StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); + if (r == NULL) + return NULL; + + r->where = pos; + r->insert_length = (short) insert_len; + r->delete_length = (short) delete_len; + + if (insert_len == 0) + { + r->char_storage = -1; + return NULL; + } + else + { + r->char_storage = state->undo_char_point; + state->undo_char_point = state->undo_char_point + (short) insert_len; + return &state->undo_char[r->char_storage]; + } } static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - StbUndoState *s = &state->undostate; - StbUndoRecord u, *r; - if (s->undo_point == 0) - return; - - // we need to do two things: apply the undo record, and create a redo record - u = s->undo_rec[s->undo_point-1]; - r = &s->undo_rec[s->redo_point-1]; - r->char_storage = -1; - - r->insert_length = u.delete_length; - r->delete_length = u.insert_length; - r->where = u.where; - - if (u.delete_length) { - // if the undo record says to delete characters, then the redo record will - // need to re-insert the characters that get deleted, so we need to store - // them. - - // there are three cases: - // there's enough room to store the characters - // characters stored for *redoing* don't leave room for redo - // characters stored for *undoing* don't leave room for redo - // if the last is true, we have to bail - - if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) { - // the undo records take up too much character space; there's no space to store the redo characters - r->insert_length = 0; - } else { - int i; - - // there's definitely room to store the characters eventually - while (s->undo_char_point + u.delete_length > s->redo_char_point) { - // there's currently not enough room, so discard a redo record - stb_textedit_discard_redo(s); - // should never happen: - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) - return; - } - r = &s->undo_rec[s->redo_point-1]; - - r->char_storage = s->redo_char_point - u.delete_length; - s->redo_char_point = s->redo_char_point - (short) u.delete_length; - - // now save the characters - for (i=0; i < u.delete_length; ++i) - s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i); - } - - // now we can carry out the deletion - STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length); - } - - // check type of recorded action: - if (u.insert_length) { - // easy case: was a deletion, so we need to insert n characters - STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length); - s->undo_char_point -= u.insert_length; - } - - state->cursor = u.where + u.insert_length; - - s->undo_point--; - s->redo_point--; + StbUndoState *s = &state->undostate; + StbUndoRecord u, *r; + if (s->undo_point == 0) + return; + + // we need to do two things: apply the undo record, and create a redo record + u = s->undo_rec[s->undo_point - 1]; + r = &s->undo_rec[s->redo_point - 1]; + r->char_storage = -1; + + r->insert_length = u.delete_length; + r->delete_length = u.insert_length; + r->where = u.where; + + if (u.delete_length) + { + // if the undo record says to delete characters, then the redo record will + // need to re-insert the characters that get deleted, so we need to store + // them. + + // there are three cases: + // there's enough room to store the characters + // characters stored for *redoing* don't leave room for redo + // characters stored for *undoing* don't leave room for redo + // if the last is true, we have to bail + + if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) + { + // the undo records take up too much character space; there's no space to store the redo characters + r->insert_length = 0; + } + else + { + int i; + + // there's definitely room to store the characters eventually + while (s->undo_char_point + u.delete_length > s->redo_char_point) + { + // there's currently not enough room, so discard a redo record + stb_textedit_discard_redo(s); + // should never happen: + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + } + r = &s->undo_rec[s->redo_point - 1]; + + r->char_storage = s->redo_char_point - u.delete_length; + s->redo_char_point = s->redo_char_point - (short) u.delete_length; + + // now save the characters + for (i = 0; i < u.delete_length; ++i) + s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i); + } + + // now we can carry out the deletion + STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length); + } + + // check type of recorded action: + if (u.insert_length) + { + // easy case: was a deletion, so we need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length); + s->undo_char_point -= u.insert_length; + } + + state->cursor = u.where + u.insert_length; + + s->undo_point--; + s->redo_point--; } static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) { - StbUndoState *s = &state->undostate; - StbUndoRecord *u, r; - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) - return; - - // we need to do two things: apply the redo record, and create an undo record - u = &s->undo_rec[s->undo_point]; - r = s->undo_rec[s->redo_point]; - - // we KNOW there must be room for the undo record, because the redo record - // was derived from an undo record - - u->delete_length = r.insert_length; - u->insert_length = r.delete_length; - u->where = r.where; - u->char_storage = -1; - - if (r.delete_length) { - // the redo record requires us to delete characters, so the undo record - // needs to store the characters - - if (s->undo_char_point + u->insert_length > s->redo_char_point) { - u->insert_length = 0; - u->delete_length = 0; - } else { - int i; - u->char_storage = s->undo_char_point; - s->undo_char_point = s->undo_char_point + u->insert_length; - - // now save the characters - for (i=0; i < u->insert_length; ++i) - s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i); - } - - STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length); - } - - if (r.insert_length) { - // easy case: need to insert n characters - STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length); - } - - state->cursor = r.where + r.insert_length; - - s->undo_point++; - s->redo_point++; + StbUndoState *s = &state->undostate; + StbUndoRecord *u, r; + if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + return; + + // we need to do two things: apply the redo record, and create an undo record + u = &s->undo_rec[s->undo_point]; + r = s->undo_rec[s->redo_point]; + + // we KNOW there must be room for the undo record, because the redo record + // was derived from an undo record + + u->delete_length = r.insert_length; + u->insert_length = r.delete_length; + u->where = r.where; + u->char_storage = -1; + + if (r.delete_length) + { + // the redo record requires us to delete characters, so the undo record + // needs to store the characters + + if (s->undo_char_point + u->insert_length > s->redo_char_point) + { + u->insert_length = 0; + u->delete_length = 0; + } + else + { + int i; + u->char_storage = s->undo_char_point; + s->undo_char_point = s->undo_char_point + u->insert_length; + + // now save the characters + for (i = 0; i < u->insert_length; ++i) + s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i); + } + + STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length); + } + + if (r.insert_length) + { + // easy case: need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length); + } + + state->cursor = r.where + r.insert_length; + + s->undo_point++; + s->redo_point++; } static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length) { - stb_text_createundo(&state->undostate, where, 0, length); + stb_text_createundo(&state->undostate, where, 0, length); } static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) { - int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); - if (p) { - for (i=0; i < length; ++i) - p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); - } + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + if (p) + { + for (i = 0; i < length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where + i); + } } static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) { - int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); - if (p) { - for (i=0; i < old_length; ++i) - p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); - } + int i; + STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + if (p) + { + for (i = 0; i < old_length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where + i); + } } // reset the state to default static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_line) { - state->undostate.undo_point = 0; - state->undostate.undo_char_point = 0; - state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; - state->select_end = state->select_start = 0; - state->cursor = 0; - state->has_preferred_x = 0; - state->preferred_x = 0; - state->cursor_at_end_of_line = 0; - state->initialized = 1; - state->single_line = (unsigned char) is_single_line; - state->insert_mode = 0; + state->undostate.undo_point = 0; + state->undostate.undo_char_point = 0; + state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->select_end = state->select_start = 0; + state->cursor = 0; + state->has_preferred_x = 0; + state->preferred_x = 0; + state->cursor_at_end_of_line = 0; + state->initialized = 1; + state->single_line = (unsigned char) is_single_line; + state->insert_mode = 0; } // API initialize static void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) { - stb_textedit_clear_state(state, is_single_line); + stb_textedit_clear_state(state, is_single_line); } -#endif//STB_TEXTEDIT_IMPLEMENTATION +#endif // STB_TEXTEDIT_IMPLEMENTATION diff --git a/attachments/simple_engine/imgui/stb_truetype.h b/attachments/simple_engine/imgui/stb_truetype.h index e6dae975..a5c3afb4 100644 --- a/attachments/simple_engine/imgui/stb_truetype.h +++ b/attachments/simple_engine/imgui/stb_truetype.h @@ -29,7 +29,7 @@ // "Zer" on mollyrocket (with fix) // Cass Everitt // stoiko (Haemimont Games) -// Brian Hook +// Brian Hook // Walter van Niftrik // David Gow // David Given @@ -217,7 +217,7 @@ // Curve tesselation 120 LOC \__ 550 LOC Bitmap creation // Bitmap management 100 LOC / // Baked bitmap interface 70 LOC / -// Font name matching & access 150 LOC ---- 150 +// Font name matching & access 150 LOC ---- 150 // C runtime library abstraction 60 LOC ---- 60 // // @@ -238,8 +238,8 @@ // Incomplete text-in-3d-api example, which draws quads properly aligned to be lossless // #if 0 -#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation -#include "stb_truetype.h" +# define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +# include "stb_truetype.h" unsigned char ttf_buffer[1<<20]; unsigned char temp_bitmap[512*512]; @@ -286,9 +286,9 @@ void my_stbtt_print(float x, float y, char *text) // Complete program (this compiles): get a single bitmap, print as ASCII art // #if 0 -#include -#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation -#include "stb_truetype.h" +# include +# define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +# include "stb_truetype.h" char ttf_buffer[1<<25]; @@ -310,7 +310,7 @@ int main(int argc, char **argv) } return 0; } -#endif +#endif // // Output: // @@ -324,9 +324,9 @@ int main(int argc, char **argv) // :@@. M@M // @@@o@@@@ // :M@@V:@@. -// +// ////////////////////////////////////////////////////////////////////////////// -// +// // Complete program: print "Hello World!" banner, with bugs // #if 0 @@ -373,7 +373,6 @@ int main(int arg, char **argv) } #endif - ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// //// @@ -383,58 +382,58 @@ int main(int arg, char **argv) //// of C library functions used by stb_truetype. #ifdef STB_TRUETYPE_IMPLEMENTATION - // #define your own (u)stbtt_int8/16/32 before including to override this - #ifndef stbtt_uint8 - typedef unsigned char stbtt_uint8; - typedef signed char stbtt_int8; - typedef unsigned short stbtt_uint16; - typedef signed short stbtt_int16; - typedef unsigned int stbtt_uint32; - typedef signed int stbtt_int32; - #endif - - typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; - typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; - - // #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h - #ifndef STBTT_ifloor - #include - #define STBTT_ifloor(x) ((int) floor(x)) - #define STBTT_iceil(x) ((int) ceil(x)) - #endif - - #ifndef STBTT_sqrt - #include - #define STBTT_sqrt(x) sqrt(x) - #endif - - #ifndef STBTT_fabs - #include - #define STBTT_fabs(x) fabs(x) - #endif - - // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h - #ifndef STBTT_malloc - #include - #define STBTT_malloc(x,u) ((void)(u),malloc(x)) - #define STBTT_free(x,u) ((void)(u),free(x)) - #endif - - #ifndef STBTT_assert - #include - #define STBTT_assert(x) assert(x) - #endif - - #ifndef STBTT_strlen - #include - #define STBTT_strlen(x) strlen(x) - #endif - - #ifndef STBTT_memcpy - #include - #define STBTT_memcpy memcpy - #define STBTT_memset memset - #endif +// #define your own (u)stbtt_int8/16/32 before including to override this +# ifndef stbtt_uint8 +typedef unsigned char stbtt_uint8; +typedef signed char stbtt_int8; +typedef unsigned short stbtt_uint16; +typedef signed short stbtt_int16; +typedef unsigned int stbtt_uint32; +typedef signed int stbtt_int32; +# endif + +typedef char stbtt__check_size32[sizeof(stbtt_int32) == 4 ? 1 : -1]; +typedef char stbtt__check_size16[sizeof(stbtt_int16) == 2 ? 1 : -1]; + +// #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h +# ifndef STBTT_ifloor +# include +# define STBTT_ifloor(x) ((int) floor(x)) +# define STBTT_iceil(x) ((int) ceil(x)) +# endif + +# ifndef STBTT_sqrt +# include +# define STBTT_sqrt(x) sqrt(x) +# endif + +# ifndef STBTT_fabs +# include +# define STBTT_fabs(x) fabs(x) +# endif + +// #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h +# ifndef STBTT_malloc +# include +# define STBTT_malloc(x, u) ((void) (u), malloc(x)) +# define STBTT_free(x, u) ((void) (u), free(x)) +# endif + +# ifndef STBTT_assert +# include +# define STBTT_assert(x) assert(x) +# endif + +# ifndef STBTT_strlen +# include +# define STBTT_strlen(x) strlen(x) +# endif + +# ifndef STBTT_memcpy +# include +# define STBTT_memcpy memcpy +# define STBTT_memset memset +# endif #endif /////////////////////////////////////////////////////////////////////////////// @@ -445,486 +444,504 @@ int main(int arg, char **argv) //// #ifndef __STB_INCLUDE_STB_TRUETYPE_H__ -#define __STB_INCLUDE_STB_TRUETYPE_H__ - -#ifdef STBTT_STATIC -#define STBTT_DEF static -#else -#define STBTT_DEF extern -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -////////////////////////////////////////////////////////////////////////////// -// -// TEXTURE BAKING API -// -// If you use this API, you only have to call two functions ever. -// - -typedef struct -{ - unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap - float xoff,yoff,xadvance; -} stbtt_bakedchar; - -STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) - float pixel_height, // height of font in pixels - unsigned char *pixels, int pw, int ph, // bitmap to be filled in - int first_char, int num_chars, // characters to bake - stbtt_bakedchar *chardata); // you allocate this, it's num_chars long -// if return is positive, the first unused row of the bitmap -// if return is negative, returns the negative of the number of characters that fit -// if return is 0, no characters fit and no rows were used -// This uses a very crappy packing. - -typedef struct -{ - float x0,y0,s0,t0; // top-left - float x1,y1,s1,t1; // bottom-right -} stbtt_aligned_quad; - -STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, // same data as above - int char_index, // character to display - float *xpos, float *ypos, // pointers to current position in screen pixel space - stbtt_aligned_quad *q, // output: quad to draw - int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier -// Call GetBakedQuad with char_index = 'character - first_char', and it -// creates the quad you need to draw and advances the current position. -// -// The coordinate system used assumes y increases downwards. -// -// Characters will extend both above and below the current position; -// see discussion of "BASELINE" above. -// -// It's inefficient; you might want to c&p it and optimize it. - - - -////////////////////////////////////////////////////////////////////////////// -// -// NEW TEXTURE BAKING API -// -// This provides options for packing multiple fonts into one atlas, not -// perfectly but better than nothing. - -typedef struct -{ - unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap - float xoff,yoff,xadvance; - float xoff2,yoff2; -} stbtt_packedchar; - -typedef struct stbtt_pack_context stbtt_pack_context; -typedef struct stbtt_fontinfo stbtt_fontinfo; -#ifndef STB_RECT_PACK_VERSION -typedef struct stbrp_rect stbrp_rect; -#endif - -STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); -// Initializes a packing context stored in the passed-in stbtt_pack_context. -// Future calls using this context will pack characters into the bitmap passed -// in here: a 1-channel bitmap that is weight x height. stride_in_bytes is -// the distance from one row to the next (or 0 to mean they are packed tightly -// together). "padding" is the amount of padding to leave between each -// character (normally you want '1' for bitmaps you'll use as textures with -// bilinear filtering). -// -// Returns 0 on failure, 1 on success. - -STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); -// Cleans up the packing context and frees all memory. - -#define STBTT_POINT_SIZE(x) (-(x)) - -STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, float font_size, - int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); -// Creates character bitmaps from the font_index'th font found in fontdata (use -// font_index=0 if you don't know what that is). It creates num_chars_in_range -// bitmaps for characters with unicode values starting at first_unicode_char_in_range -// and increasing. Data for how to render them is stored in chardata_for_range; -// pass these to stbtt_GetPackedQuad to get back renderable quads. -// -// font_size is the full height of the character from ascender to descender, -// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed -// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() -// and pass that result as 'font_size': -// ..., 20 , ... // font max minus min y is 20 pixels tall -// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall - -typedef struct -{ - float font_size; - int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint - int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints - int num_chars; - stbtt_packedchar *chardata_for_range; // output - unsigned char h_oversample, v_oversample; // don't set these, they're used internally -} stbtt_pack_range; - -STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); -// Creates character bitmaps from multiple ranges of characters stored in -// ranges. This will usually create a better-packed bitmap than multiple -// calls to stbtt_PackFontRange. Note that you can call this multiple -// times within a single PackBegin/PackEnd. - -STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); -// Oversampling a font increases the quality by allowing higher-quality subpixel -// positioning, and is especially valuable at smaller text sizes. -// -// This function sets the amount of oversampling for all following calls to -// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given -// pack context. The default (no oversampling) is achieved by h_oversample=1 -// and v_oversample=1. The total number of pixels required is -// h_oversample*v_oversample larger than the default; for example, 2x2 -// oversampling requires 4x the storage of 1x1. For best results, render -// oversampled textures with bilinear filtering. Look at the readme in -// stb/tests/oversample for information about oversampled fonts -// -// To use with PackFontRangesGather etc., you must set it before calls -// call to PackFontRangesGatherRects. - -STBTT_DEF void stbtt_GetPackedQuad(stbtt_packedchar *chardata, int pw, int ph, // same data as above - int char_index, // character to display - float *xpos, float *ypos, // pointers to current position in screen pixel space - stbtt_aligned_quad *q, // output: quad to draw - int align_to_integer); - -STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); -STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); -STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); -// Calling these functions in sequence is roughly equivalent to calling -// stbtt_PackFontRanges(). If you more control over the packing of multiple -// fonts, or if you want to pack custom data into a font texture, take a look -// at the source to of stbtt_PackFontRanges() and create a custom version -// using these functions, e.g. call GatherRects multiple times, -// building up a single array of rects, then call PackRects once, -// then call RenderIntoRects repeatedly. This may result in a -// better packing than calling PackFontRanges multiple times -// (or it may not). - -// this is an opaque structure that you shouldn't mess with which holds -// all the context needed from PackBegin to PackEnd. -struct stbtt_pack_context { - void *user_allocator_context; - void *pack_info; - int width; - int height; - int stride_in_bytes; - int padding; - unsigned int h_oversample, v_oversample; - unsigned char *pixels; - void *nodes; -}; - -////////////////////////////////////////////////////////////////////////////// -// -// FONT LOADING -// -// - -STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); -// Each .ttf/.ttc file may have more than one font. Each font has a sequential -// index number starting from 0. Call this function to get the font offset for -// a given index; it returns -1 if the index is out of range. A regular .ttf -// file will only define one font and it always be at offset 0, so it will -// return '0' for index 0, and -1 for all other indices. You can just skip -// this step if you know it's that kind of font. - - -// The following structure is defined publically so you can declare one on -// the stack or as a global or etc, but you should treat it as opaque. -struct stbtt_fontinfo -{ - void * userdata; - unsigned char * data; // pointer to .ttf file - int fontstart; // offset of start of font - - int numGlyphs; // number of glyphs, needed for range checking - - int loca,head,glyf,hhea,hmtx,kern; // table locations as offset from start of .ttf - int index_map; // a cmap mapping for our chosen character encoding - int indexToLocFormat; // format needed to map from glyph index to glyph -}; - -STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); -// Given an offset into the file that defines a font, this function builds -// the necessary cached info for the rest of the system. You must allocate -// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't -// need to do anything special to free it, because the contents are pure -// value data with no additional data structures. Returns 0 on failure. - - -////////////////////////////////////////////////////////////////////////////// -// -// CHARACTER TO GLYPH-INDEX CONVERSIOn - -STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); -// If you're going to perform multiple operations on the same character -// and you want a speed-up, call this function with the character you're -// going to process, then use glyph-based functions instead of the -// codepoint-based functions. - - -////////////////////////////////////////////////////////////////////////////// -// -// CHARACTER PROPERTIES -// - -STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); -// computes a scale factor to produce a font whose "height" is 'pixels' tall. -// Height is measured as the distance from the highest ascender to the lowest -// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics -// and computing: -// scale = pixels / (ascent - descent) -// so if you prefer to measure height by the ascent only, use a similar calculation. - -STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); -// computes a scale factor to produce a font whose EM size is mapped to -// 'pixels' tall. This is probably what traditional APIs compute, but -// I'm not positive. - -STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); -// ascent is the coordinate above the baseline the font extends; descent -// is the coordinate below the baseline the font extends (i.e. it is typically negative) -// lineGap is the spacing between one row's descent and the next row's ascent... -// so you should advance the vertical position by "*ascent - *descent + *lineGap" -// these are expressed in unscaled coordinates, so you must multiply by -// the scale factor for a given size - -STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); -// the bounding box around all possible characters - -STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); -// leftSideBearing is the offset from the current horizontal position to the left edge of the character -// advanceWidth is the offset from the current horizontal position to the next horizontal position -// these are expressed in unscaled coordinates - -STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); -// an additional amount to add to the 'advance' value between ch1 and ch2 - -STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); -// Gets the bounding box of the visible part of the glyph, in unscaled coordinates - -STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); -STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); -STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); -// as above, but takes one or more glyph indices for greater efficiency - - -////////////////////////////////////////////////////////////////////////////// -// -// GLYPH SHAPES (you probably don't need these, but they have to go before -// the bitmaps for C declaration-order reasons) -// - -#ifndef STBTT_vmove // you can predefine these to use different values (but why?) - enum { - STBTT_vmove=1, - STBTT_vline, - STBTT_vcurve - }; -#endif - -#ifndef stbtt_vertex // you can predefine this to use different values - // (we share this with other code at RAD) - #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file - typedef struct - { - stbtt_vertex_type x,y,cx,cy; - unsigned char type,padding; - } stbtt_vertex; -#endif - -STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); -// returns non-zero if nothing is drawn for this glyph - -STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); -STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); -// returns # of vertices and fills *vertices with the pointer to them -// these are expressed in "unscaled" coordinates -// -// The shape is a series of countours. Each one starts with -// a STBTT_moveto, then consists of a series of mixed -// STBTT_lineto and STBTT_curveto segments. A lineto -// draws a line from previous endpoint to its x,y; a curveto -// draws a quadratic bezier from previous endpoint to -// its x,y, using cx,cy as the bezier control point. - -STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); -// frees the data allocated above - -////////////////////////////////////////////////////////////////////////////// -// -// BITMAP RENDERING -// - -STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); -// frees the bitmap allocated below - -STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); -// allocates a large-enough single-channel 8bpp bitmap and renders the -// specified character/glyph at the specified scale into it, with -// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). -// *width & *height are filled out with the width & height of the bitmap, -// which is stored left-to-right, top-to-bottom. -// -// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap - -STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); -// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel -// shift for the character - -STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); -// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap -// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap -// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the -// width and height and positioning info for it first. - -STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); -// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel -// shift for the character - -STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); -// get the bbox of the bitmap centered around the glyph origin; so the -// bitmap width is ix1-ix0, height is iy1-iy0, and location to place -// the bitmap top left is (leftSideBearing*scale,iy0). -// (Note that the bitmap uses y-increases-down, but the shape uses -// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) - -STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); -// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel -// shift for the character - -// the following functions are equivalent to the above functions, but operate -// on glyph indices instead of Unicode codepoints (for efficiency) -STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); -STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); -STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); -STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); -STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); -STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); - - -// @TODO: don't expose this structure -typedef struct -{ - int w,h,stride; - unsigned char *pixels; -} stbtt__bitmap; - -// rasterize a shape with quadratic beziers into a bitmap -STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into - float flatness_in_pixels, // allowable error of curve in pixels - stbtt_vertex *vertices, // array of vertices defining shape - int num_verts, // number of vertices in above array - float scale_x, float scale_y, // scale applied to input vertices - float shift_x, float shift_y, // translation applied to input vertices - int x_off, int y_off, // another translation applied to input - int invert, // if non-zero, vertically flip shape - void *userdata); // context for to STBTT_MALLOC - -////////////////////////////////////////////////////////////////////////////// -// -// Finding the right font... -// -// You should really just solve this offline, keep your own tables -// of what font is what, and don't try to get it out of the .ttf file. -// That's because getting it out of the .ttf file is really hard, because -// the names in the file can appear in many possible encodings, in many -// possible languages, and e.g. if you need a case-insensitive comparison, -// the details of that depend on the encoding & language in a complex way -// (actually underspecified in truetype, but also gigantic). -// -// But you can use the provided functions in two possible ways: -// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on -// unicode-encoded names to try to find the font you want; -// you can run this before calling stbtt_InitFont() -// -// stbtt_GetFontNameString() lets you get any of the various strings -// from the file yourself and do your own comparisons on them. -// You have to have called stbtt_InitFont() first. - - -STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +# define __STB_INCLUDE_STB_TRUETYPE_H__ + +# ifdef STBTT_STATIC +# define STBTT_DEF static +# else +# define STBTT_DEF extern +# endif + +# ifdef __cplusplus +extern "C" +{ +# endif + + ////////////////////////////////////////////////////////////////////////////// + // + // TEXTURE BAKING API + // + // If you use this API, you only have to call two functions ever. + // + + typedef struct + { + unsigned short x0, y0, x1, y1; // coordinates of bbox in bitmap + float xoff, yoff, xadvance; + } stbtt_bakedchar; + + STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long + // if return is positive, the first unused row of the bitmap + // if return is negative, returns the negative of the number of characters that fit + // if return is 0, no characters fit and no rows were used + // This uses a very crappy packing. + + typedef struct + { + float x0, y0, s0, t0; // top-left + float x1, y1, s1, t1; // bottom-right + } stbtt_aligned_quad; + + STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier + // Call GetBakedQuad with char_index = 'character - first_char', and it + // creates the quad you need to draw and advances the current position. + // + // The coordinate system used assumes y increases downwards. + // + // Characters will extend both above and below the current position; + // see discussion of "BASELINE" above. + // + // It's inefficient; you might want to c&p it and optimize it. + + ////////////////////////////////////////////////////////////////////////////// + // + // NEW TEXTURE BAKING API + // + // This provides options for packing multiple fonts into one atlas, not + // perfectly but better than nothing. + + typedef struct + { + unsigned short x0, y0, x1, y1; // coordinates of bbox in bitmap + float xoff, yoff, xadvance; + float xoff2, yoff2; + } stbtt_packedchar; + + typedef struct stbtt_pack_context stbtt_pack_context; + typedef struct stbtt_fontinfo stbtt_fontinfo; +# ifndef STB_RECT_PACK_VERSION + typedef struct stbrp_rect stbrp_rect; +# endif + + STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); + // Initializes a packing context stored in the passed-in stbtt_pack_context. + // Future calls using this context will pack characters into the bitmap passed + // in here: a 1-channel bitmap that is weight x height. stride_in_bytes is + // the distance from one row to the next (or 0 to mean they are packed tightly + // together). "padding" is the amount of padding to leave between each + // character (normally you want '1' for bitmaps you'll use as textures with + // bilinear filtering). + // + // Returns 0 on failure, 1 on success. + + STBTT_DEF void stbtt_PackEnd(stbtt_pack_context *spc); + // Cleans up the packing context and frees all memory. + +# define STBTT_POINT_SIZE(x) (-(x)) + + STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); + // Creates character bitmaps from the font_index'th font found in fontdata (use + // font_index=0 if you don't know what that is). It creates num_chars_in_range + // bitmaps for characters with unicode values starting at first_unicode_char_in_range + // and increasing. Data for how to render them is stored in chardata_for_range; + // pass these to stbtt_GetPackedQuad to get back renderable quads. + // + // font_size is the full height of the character from ascender to descender, + // as computed by stbtt_ScaleForPixelHeight. To use a point size as computed + // by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() + // and pass that result as 'font_size': + // ..., 20 , ... // font max minus min y is 20 pixels tall + // ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + + typedef struct + { + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally + } stbtt_pack_range; + + STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); + // Creates character bitmaps from multiple ranges of characters stored in + // ranges. This will usually create a better-packed bitmap than multiple + // calls to stbtt_PackFontRange. Note that you can call this multiple + // times within a single PackBegin/PackEnd. + + STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); + // Oversampling a font increases the quality by allowing higher-quality subpixel + // positioning, and is especially valuable at smaller text sizes. + // + // This function sets the amount of oversampling for all following calls to + // stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given + // pack context. The default (no oversampling) is achieved by h_oversample=1 + // and v_oversample=1. The total number of pixels required is + // h_oversample*v_oversample larger than the default; for example, 2x2 + // oversampling requires 4x the storage of 1x1. For best results, render + // oversampled textures with bilinear filtering. Look at the readme in + // stb/tests/oversample for information about oversampled fonts + // + // To use with PackFontRangesGather etc., you must set it before calls + // call to PackFontRangesGatherRects. + + STBTT_DEF void stbtt_GetPackedQuad(stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + + STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); + STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); + STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); + // Calling these functions in sequence is roughly equivalent to calling + // stbtt_PackFontRanges(). If you more control over the packing of multiple + // fonts, or if you want to pack custom data into a font texture, take a look + // at the source to of stbtt_PackFontRanges() and create a custom version + // using these functions, e.g. call GatherRects multiple times, + // building up a single array of rects, then call PackRects once, + // then call RenderIntoRects repeatedly. This may result in a + // better packing than calling PackFontRanges multiple times + // (or it may not). + + // this is an opaque structure that you shouldn't mess with which holds + // all the context needed from PackBegin to PackEnd. + struct stbtt_pack_context + { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; + }; + + ////////////////////////////////////////////////////////////////////////////// + // + // FONT LOADING + // + // + + STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); + // Each .ttf/.ttc file may have more than one font. Each font has a sequential + // index number starting from 0. Call this function to get the font offset for + // a given index; it returns -1 if the index is out of range. A regular .ttf + // file will only define one font and it always be at offset 0, so it will + // return '0' for index 0, and -1 for all other indices. You can just skip + // this step if you know it's that kind of font. + + // The following structure is defined publically so you can declare one on + // the stack or as a global or etc, but you should treat it as opaque. + struct stbtt_fontinfo + { + void *userdata; + unsigned char *data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca, head, glyf, hhea, hmtx, kern; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph + }; + + STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); + // Given an offset into the file that defines a font, this function builds + // the necessary cached info for the rest of the system. You must allocate + // the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't + // need to do anything special to free it, because the contents are pure + // value data with no additional data structures. Returns 0 on failure. + + ////////////////////////////////////////////////////////////////////////////// + // + // CHARACTER TO GLYPH-INDEX CONVERSIOn + + STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); + // If you're going to perform multiple operations on the same character + // and you want a speed-up, call this function with the character you're + // going to process, then use glyph-based functions instead of the + // codepoint-based functions. + + ////////////////////////////////////////////////////////////////////////////// + // + // CHARACTER PROPERTIES + // + + STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); + // computes a scale factor to produce a font whose "height" is 'pixels' tall. + // Height is measured as the distance from the highest ascender to the lowest + // descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics + // and computing: + // scale = pixels / (ascent - descent) + // so if you prefer to measure height by the ascent only, use a similar calculation. + + STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); + // computes a scale factor to produce a font whose EM size is mapped to + // 'pixels' tall. This is probably what traditional APIs compute, but + // I'm not positive. + + STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); + // ascent is the coordinate above the baseline the font extends; descent + // is the coordinate below the baseline the font extends (i.e. it is typically negative) + // lineGap is the spacing between one row's descent and the next row's ascent... + // so you should advance the vertical position by "*ascent - *descent + *lineGap" + // these are expressed in unscaled coordinates, so you must multiply by + // the scale factor for a given size + + STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); + // the bounding box around all possible characters + + STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); + // leftSideBearing is the offset from the current horizontal position to the left edge of the character + // advanceWidth is the offset from the current horizontal position to the next horizontal position + // these are expressed in unscaled coordinates + + STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); + // an additional amount to add to the 'advance' value between ch1 and ch2 + + STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); + // Gets the bounding box of the visible part of the glyph, in unscaled coordinates + + STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); + STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); + STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); + // as above, but takes one or more glyph indices for greater efficiency + + ////////////////////////////////////////////////////////////////////////////// + // + // GLYPH SHAPES (you probably don't need these, but they have to go before + // the bitmaps for C declaration-order reasons) + // + +# ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum + { + STBTT_vmove = 1, + STBTT_vline, + STBTT_vcurve + }; +# endif + +# ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) +# define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x, y, cx, cy; + unsigned char type, padding; + } stbtt_vertex; +# endif + + STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); + // returns non-zero if nothing is drawn for this glyph + + STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); + STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); + // returns # of vertices and fills *vertices with the pointer to them + // these are expressed in "unscaled" coordinates + // + // The shape is a series of countours. Each one starts with + // a STBTT_moveto, then consists of a series of mixed + // STBTT_lineto and STBTT_curveto segments. A lineto + // draws a line from previous endpoint to its x,y; a curveto + // draws a quadratic bezier from previous endpoint to + // its x,y, using cx,cy as the bezier control point. + + STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); + // frees the data allocated above + + ////////////////////////////////////////////////////////////////////////////// + // + // BITMAP RENDERING + // + + STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); + // frees the bitmap allocated below + + STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); + // allocates a large-enough single-channel 8bpp bitmap and renders the + // specified character/glyph at the specified scale into it, with + // antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). + // *width & *height are filled out with the width & height of the bitmap, + // which is stored left-to-right, top-to-bottom. + // + // xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + + STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); + // the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel + // shift for the character + + STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); + // the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap + // in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap + // is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the + // width and height and positioning info for it first. + + STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); + // same as stbtt_MakeCodepointBitmap, but you can specify a subpixel + // shift for the character + + STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); + // get the bbox of the bitmap centered around the glyph origin; so the + // bitmap width is ix1-ix0, height is iy1-iy0, and location to place + // the bitmap top left is (leftSideBearing*scale,iy0). + // (Note that the bitmap uses y-increases-down, but the shape uses + // y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + + STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + // same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel + // shift for the character + + // the following functions are equivalent to the above functions, but operate + // on glyph indices instead of Unicode codepoints (for efficiency) + STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); + STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); + STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); + STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); + STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); + STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + // @TODO: don't expose this structure + typedef struct + { + int w, h, stride; + unsigned char *pixels; + } stbtt__bitmap; + + // rasterize a shape with quadratic beziers into a bitmap + STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + + ////////////////////////////////////////////////////////////////////////////// + // + // Finding the right font... + // + // You should really just solve this offline, keep your own tables + // of what font is what, and don't try to get it out of the .ttf file. + // That's because getting it out of the .ttf file is really hard, because + // the names in the file can appear in many possible encodings, in many + // possible languages, and e.g. if you need a case-insensitive comparison, + // the details of that depend on the encoding & language in a complex way + // (actually underspecified in truetype, but also gigantic). + // + // But you can use the provided functions in two possible ways: + // stbtt_FindMatchingFont() will use *case-sensitive* comparisons on + // unicode-encoded names to try to find the font you want; + // you can run this before calling stbtt_InitFont() + // + // stbtt_GetFontNameString() lets you get any of the various strings + // from the file yourself and do your own comparisons on them. + // You have to have called stbtt_InitFont() first. + + STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); // returns the offset (not index) of the font that matches, or -1 if none // if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". // if you use any other flag, use a font name like "Arial"; this checks // the 'macStyle' header field; i don't know if fonts set this consistently -#define STBTT_MACSTYLE_DONTCARE 0 -#define STBTT_MACSTYLE_BOLD 1 -#define STBTT_MACSTYLE_ITALIC 2 -#define STBTT_MACSTYLE_UNDERSCORE 4 -#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 - -STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); -// returns 1/0 whether the first string interpreted as utf8 is identical to -// the second string interpreted as big-endian utf16... useful for strings from next func - -STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); -// returns the string (which may be big-endian double byte, e.g. for unicode) -// and puts the length in bytes in *length. -// -// some of the values for the IDs are below; for more see the truetype spec: -// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html -// http://www.microsoft.com/typography/otspec/name.htm - -enum { // platformID - STBTT_PLATFORM_ID_UNICODE =0, - STBTT_PLATFORM_ID_MAC =1, - STBTT_PLATFORM_ID_ISO =2, - STBTT_PLATFORM_ID_MICROSOFT =3 -}; - -enum { // encodingID for STBTT_PLATFORM_ID_UNICODE - STBTT_UNICODE_EID_UNICODE_1_0 =0, - STBTT_UNICODE_EID_UNICODE_1_1 =1, - STBTT_UNICODE_EID_ISO_10646 =2, - STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, - STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 -}; - -enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT - STBTT_MS_EID_SYMBOL =0, - STBTT_MS_EID_UNICODE_BMP =1, - STBTT_MS_EID_SHIFTJIS =2, - STBTT_MS_EID_UNICODE_FULL =10 -}; - -enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes - STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, - STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, - STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, - STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 -}; - -enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... - // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs - STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, - STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, - STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, - STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, - STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, - STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D -}; - -enum { // languageID for STBTT_PLATFORM_ID_MAC - STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, - STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, - STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, - STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , - STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , - STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, - STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 -}; - -#ifdef __cplusplus -} -#endif - -#endif // __STB_INCLUDE_STB_TRUETYPE_H__ +# define STBTT_MACSTYLE_DONTCARE 0 +# define STBTT_MACSTYLE_BOLD 1 +# define STBTT_MACSTYLE_ITALIC 2 +# define STBTT_MACSTYLE_UNDERSCORE 4 +# define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + + STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); + // returns 1/0 whether the first string interpreted as utf8 is identical to + // the second string interpreted as big-endian utf16... useful for strings from next func + + STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); + // returns the string (which may be big-endian double byte, e.g. for unicode) + // and puts the length in bytes in *length. + // + // some of the values for the IDs are below; for more see the truetype spec: + // http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html + // http://www.microsoft.com/typography/otspec/name.htm + + enum + { // platformID + STBTT_PLATFORM_ID_UNICODE = 0, + STBTT_PLATFORM_ID_MAC = 1, + STBTT_PLATFORM_ID_ISO = 2, + STBTT_PLATFORM_ID_MICROSOFT = 3 + }; + + enum + { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 = 0, + STBTT_UNICODE_EID_UNICODE_1_1 = 1, + STBTT_UNICODE_EID_ISO_10646 = 2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP = 3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL = 4 + }; + + enum + { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL = 0, + STBTT_MS_EID_UNICODE_BMP = 1, + STBTT_MS_EID_SHIFTJIS = 2, + STBTT_MS_EID_UNICODE_FULL = 10 + }; + + enum + { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN = 0, + STBTT_MAC_EID_ARABIC = 4, + STBTT_MAC_EID_JAPANESE = 1, + STBTT_MAC_EID_HEBREW = 5, + STBTT_MAC_EID_CHINESE_TRAD = 2, + STBTT_MAC_EID_GREEK = 6, + STBTT_MAC_EID_KOREAN = 3, + STBTT_MAC_EID_RUSSIAN = 7 + }; + + enum + { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH = 0x0409, + STBTT_MS_LANG_ITALIAN = 0x0410, + STBTT_MS_LANG_CHINESE = 0x0804, + STBTT_MS_LANG_JAPANESE = 0x0411, + STBTT_MS_LANG_DUTCH = 0x0413, + STBTT_MS_LANG_KOREAN = 0x0412, + STBTT_MS_LANG_FRENCH = 0x040c, + STBTT_MS_LANG_RUSSIAN = 0x0419, + STBTT_MS_LANG_GERMAN = 0x0407, + STBTT_MS_LANG_SPANISH = 0x0409, + STBTT_MS_LANG_HEBREW = 0x040d, + STBTT_MS_LANG_SWEDISH = 0x041D + }; + + enum + { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH = 0, + STBTT_MAC_LANG_JAPANESE = 11, + STBTT_MAC_LANG_ARABIC = 12, + STBTT_MAC_LANG_KOREAN = 23, + STBTT_MAC_LANG_DUTCH = 4, + STBTT_MAC_LANG_RUSSIAN = 32, + STBTT_MAC_LANG_FRENCH = 1, + STBTT_MAC_LANG_SPANISH = 6, + STBTT_MAC_LANG_GERMAN = 2, + STBTT_MAC_LANG_SWEDISH = 5, + STBTT_MAC_LANG_HEBREW = 10, + STBTT_MAC_LANG_CHINESE_SIMPLIFIED = 33, + STBTT_MAC_LANG_ITALIAN = 3, + STBTT_MAC_LANG_CHINESE_TRAD = 19 + }; + +# ifdef __cplusplus +} +# endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// @@ -935,19 +952,19 @@ enum { // languageID for STBTT_PLATFORM_ID_MAC #ifdef STB_TRUETYPE_IMPLEMENTATION -#ifndef STBTT_MAX_OVERSAMPLE -#define STBTT_MAX_OVERSAMPLE 8 -#endif +# ifndef STBTT_MAX_OVERSAMPLE +# define STBTT_MAX_OVERSAMPLE 8 +# endif -#if STBTT_MAX_OVERSAMPLE > 255 -#error "STBTT_MAX_OVERSAMPLE cannot be > 255" -#endif +# if STBTT_MAX_OVERSAMPLE > 255 +# error "STBTT_MAX_OVERSAMPLE cannot be > 255" +# endif -typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE - 1)) == 0 ? 1 : -1]; -#ifndef STBTT_RASTERIZER_VERSION -#define STBTT_RASTERIZER_VERSION 2 -#endif +# ifndef STBTT_RASTERIZER_VERSION +# define STBTT_RASTERIZER_VERSION 2 +# endif ////////////////////////////////////////////////////////////////////////// // @@ -957,606 +974,733 @@ typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERS // on platforms that don't allow misaligned reads, if we want to allow // truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE -#define ttBYTE(p) (* (stbtt_uint8 *) (p)) -#define ttCHAR(p) (* (stbtt_int8 *) (p)) -#define ttFixed(p) ttLONG(p) +# define ttBYTE(p) (*(stbtt_uint8 *) (p)) +# define ttCHAR(p) (*(stbtt_int8 *) (p)) +# define ttFixed(p) ttLONG(p) -#if defined(STB_TRUETYPE_BIGENDIAN) && !defined(ALLOW_UNALIGNED_TRUETYPE) +# if defined(STB_TRUETYPE_BIGENDIAN) && !defined(ALLOW_UNALIGNED_TRUETYPE) - #define ttUSHORT(p) (* (stbtt_uint16 *) (p)) - #define ttSHORT(p) (* (stbtt_int16 *) (p)) - #define ttULONG(p) (* (stbtt_uint32 *) (p)) - #define ttLONG(p) (* (stbtt_int32 *) (p)) +# define ttUSHORT(p) (*(stbtt_uint16 *) (p)) +# define ttSHORT(p) (*(stbtt_int16 *) (p)) +# define ttULONG(p) (*(stbtt_uint32 *) (p)) +# define ttLONG(p) (*(stbtt_int32 *) (p)) -#else +# else - static stbtt_uint16 ttUSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } - static stbtt_int16 ttSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } - static stbtt_uint32 ttULONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } - static stbtt_int32 ttLONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } +static stbtt_uint16 ttUSHORT(const stbtt_uint8 *p) +{ + return p[0] * 256 + p[1]; +} +static stbtt_int16 ttSHORT(const stbtt_uint8 *p) +{ + return p[0] * 256 + p[1]; +} +static stbtt_uint32 ttULONG(const stbtt_uint8 *p) +{ + return (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + p[3]; +} +static stbtt_int32 ttLONG(const stbtt_uint8 *p) +{ + return (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + p[3]; +} -#endif +# endif -#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) -#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) +# define stbtt_tag4(p, c0, c1, c2, c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +# define stbtt_tag(p, str) stbtt_tag4(p, str[0], str[1], str[2], str[3]) static int stbtt__isfont(const stbtt_uint8 *font) { - // check the version number - if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 - if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! - if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF - if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 - return 0; + // check the version number + if (stbtt_tag4(font, '1', 0, 0, 0)) + return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) + return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) + return 1; // OpenType with CFF + if (stbtt_tag4(font, 0, 1, 0, 0)) + return 1; // OpenType 1.0 + return 0; } // @OPTIMIZE: binary search static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) { - stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); - stbtt_uint32 tabledir = fontstart + 12; - stbtt_int32 i; - for (i=0; i < num_tables; ++i) { - stbtt_uint32 loc = tabledir + 16*i; - if (stbtt_tag(data+loc+0, tag)) - return ttULONG(data+loc+8); - } - return 0; + stbtt_int32 num_tables = ttUSHORT(data + fontstart + 4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i = 0; i < num_tables; ++i) + { + stbtt_uint32 loc = tabledir + 16 * i; + if (stbtt_tag(data + loc + 0, tag)) + return ttULONG(data + loc + 8); + } + return 0; } STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *font_collection, int index) { - // if it's just a font, there's only one valid index - if (stbtt__isfont(font_collection)) - return index == 0 ? 0 : -1; - - // check if it's a TTC - if (stbtt_tag(font_collection, "ttcf")) { - // version 1? - if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { - stbtt_int32 n = ttLONG(font_collection+8); - if (index >= n) - return -1; - return ttULONG(font_collection+12+index*4); - } - } - return -1; + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) + { + // version 1? + if (ttULONG(font_collection + 4) == 0x00010000 || ttULONG(font_collection + 4) == 0x00020000) + { + stbtt_int32 n = ttLONG(font_collection + 8); + if (index >= n) + return -1; + return ttULONG(font_collection + 12 + index * 4); + } + } + return -1; } STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data2, int fontstart) { - stbtt_uint8 *data = (stbtt_uint8 *) data2; - stbtt_uint32 cmap, t; - stbtt_int32 i,numTables; - - info->data = data; - info->fontstart = fontstart; - - cmap = stbtt__find_table(data, fontstart, "cmap"); // required - info->loca = stbtt__find_table(data, fontstart, "loca"); // required - info->head = stbtt__find_table(data, fontstart, "head"); // required - info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required - info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required - info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required - info->kern = stbtt__find_table(data, fontstart, "kern"); // not required - if (!cmap || !info->loca || !info->head || !info->glyf || !info->hhea || !info->hmtx) - return 0; - - t = stbtt__find_table(data, fontstart, "maxp"); - if (t) - info->numGlyphs = ttUSHORT(data+t+4); - else - info->numGlyphs = 0xffff; - - // find a cmap encoding table we understand *now* to avoid searching - // later. (todo: could make this installable) - // the same regardless of glyph. - numTables = ttUSHORT(data + cmap + 2); - info->index_map = 0; - for (i=0; i < numTables; ++i) { - stbtt_uint32 encoding_record = cmap + 4 + 8 * i; - // find an encoding we understand: - switch(ttUSHORT(data+encoding_record)) { - case STBTT_PLATFORM_ID_MICROSOFT: - switch (ttUSHORT(data+encoding_record+2)) { - case STBTT_MS_EID_UNICODE_BMP: - case STBTT_MS_EID_UNICODE_FULL: - // MS/Unicode - info->index_map = cmap + ttULONG(data+encoding_record+4); - break; - } - break; - case STBTT_PLATFORM_ID_UNICODE: - // Mac/iOS has these - // all the encodingIDs are unicode, so we don't bother to check it - info->index_map = cmap + ttULONG(data+encoding_record+4); - break; - } - } - if (info->index_map == 0) - return 0; - - info->indexToLocFormat = ttUSHORT(data+info->head + 50); - return 1; + stbtt_uint8 *data = (stbtt_uint8 *) data2; + stbtt_uint32 cmap, t; + stbtt_int32 i, numTables; + + info->data = data; + info->fontstart = fontstart; + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + if (!cmap || !info->loca || !info->head || !info->glyf || !info->hhea || !info->hmtx) + return 0; + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data + t + 4); + else + info->numGlyphs = 0xffff; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i = 0; i < numTables; ++i) + { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch (ttUSHORT(data + encoding_record)) + { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data + encoding_record + 2)) + { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data + encoding_record + 4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data + encoding_record + 4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data + info->head + 50); + return 1; } STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) { - stbtt_uint8 *data = info->data; - stbtt_uint32 index_map = info->index_map; - - stbtt_uint16 format = ttUSHORT(data + index_map + 0); - if (format == 0) { // apple byte encoding - stbtt_int32 bytes = ttUSHORT(data + index_map + 2); - if (unicode_codepoint < bytes-6) - return ttBYTE(data + index_map + 6 + unicode_codepoint); - return 0; - } else if (format == 6) { - stbtt_uint32 first = ttUSHORT(data + index_map + 6); - stbtt_uint32 count = ttUSHORT(data + index_map + 8); - if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) - return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); - return 0; - } else if (format == 2) { - STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean - return 0; - } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges - stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; - stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; - stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); - stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; - - // do a binary search of the segments - stbtt_uint32 endCount = index_map + 14; - stbtt_uint32 search = endCount; - - if (unicode_codepoint > 0xffff) - return 0; - - // they lie from endCount .. endCount + segCount - // but searchRange is the nearest power of two, so... - if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) - search += rangeShift*2; - - // now decrement to bias correctly to find smallest - search -= 2; - while (entrySelector) { - stbtt_uint16 end; - searchRange >>= 1; - end = ttUSHORT(data + search + searchRange*2); - if (unicode_codepoint > end) - search += searchRange*2; - --entrySelector; - } - search += 2; - - { - stbtt_uint16 offset, start; - stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); - - STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item)); - start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); - if (unicode_codepoint < start) - return 0; - - offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); - if (offset == 0) - return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); - - return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); - } - } else if (format == 12 || format == 13) { - stbtt_uint32 ngroups = ttULONG(data+index_map+12); - stbtt_int32 low,high; - low = 0; high = (stbtt_int32)ngroups; - // Binary search the right group. - while (low < high) { - stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high - stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); - stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); - if ((stbtt_uint32) unicode_codepoint < start_char) - high = mid; - else if ((stbtt_uint32) unicode_codepoint > end_char) - low = mid+1; - else { - stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); - if (format == 12) - return start_glyph + unicode_codepoint-start_char; - else // format == 13 - return start_glyph; - } - } - return 0; // not found - } - // @TODO - STBTT_assert(0); - return 0; + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) + { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes - 6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } + else if (format == 6) + { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first + count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first) * 2); + return 0; + } + else if (format == 2) + { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } + else if (format == 4) + { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data + index_map + 6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data + index_map + 8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data + index_map + 10); + stbtt_uint16 rangeShift = ttUSHORT(data + index_map + 12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift * 2)) + search += rangeShift * 2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) + { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange * 2); + if (unicode_codepoint > end) + search += searchRange * 2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2 * item)); + start = ttUSHORT(data + index_map + 14 + segcount * 2 + 2 + 2 * item); + if (unicode_codepoint < start) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount * 6 + 2 + 2 * item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount * 4 + 2 + 2 * item)); + + return ttUSHORT(data + offset + (unicode_codepoint - start) * 2 + index_map + 14 + segcount * 6 + 2 + 2 * item); + } + } + else if (format == 12 || format == 13) + { + stbtt_uint32 ngroups = ttULONG(data + index_map + 12); + stbtt_int32 low, high; + low = 0; + high = (stbtt_int32) ngroups; + // Binary search the right group. + while (low < high) + { + stbtt_int32 mid = low + ((high - low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data + index_map + 16 + mid * 12); + stbtt_uint32 end_char = ttULONG(data + index_map + 16 + mid * 12 + 4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid + 1; + else + { + stbtt_uint32 start_glyph = ttULONG(data + index_map + 16 + mid * 12 + 8); + if (format == 12) + return start_glyph + unicode_codepoint - start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; } STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) { - return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); } static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) { - v->type = type; - v->x = (stbtt_int16) x; - v->y = (stbtt_int16) y; - v->cx = (stbtt_int16) cx; - v->cy = (stbtt_int16) cy; + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; } static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) { - int g1,g2; + int g1, g2; - if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range - if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + if (glyph_index >= info->numGlyphs) + return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) + return -1; // unknown index->glyph map format - if (info->indexToLocFormat == 0) { - g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; - g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; - } else { - g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); - g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); - } + if (info->indexToLocFormat == 0) + { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } + else + { + g1 = info->glyf + ttULONG(info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG(info->data + info->loca + glyph_index * 4 + 4); + } - return g1==g2 ? -1 : g1; // if length is 0, return -1 + return g1 == g2 ? -1 : g1; // if length is 0, return -1 } STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) { - int g = stbtt__GetGlyfOffset(info, glyph_index); - if (g < 0) return 0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) + return 0; - if (x0) *x0 = ttSHORT(info->data + g + 2); - if (y0) *y0 = ttSHORT(info->data + g + 4); - if (x1) *x1 = ttSHORT(info->data + g + 6); - if (y1) *y1 = ttSHORT(info->data + g + 8); - return 1; + if (x0) + *x0 = ttSHORT(info->data + g + 2); + if (y0) + *y0 = ttSHORT(info->data + g + 4); + if (x1) + *x1 = ttSHORT(info->data + g + 6); + if (y1) + *y1 = ttSHORT(info->data + g + 8); + return 1; } STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) { - return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info, codepoint), x0, y0, x1, y1); } STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) { - stbtt_int16 numberOfContours; - int g = stbtt__GetGlyfOffset(info, glyph_index); - if (g < 0) return 1; - numberOfContours = ttSHORT(info->data + g); - return numberOfContours == 0; + stbtt_int16 numberOfContours; + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) + return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; } static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, - stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) -{ - if (start_off) { - if (was_off) - stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); - stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); - } else { - if (was_off) - stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); - else - stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); - } - return num_vertices; + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) + { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx + scx) >> 1, (cy + scy) >> 1, cx, cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx, sy, scx, scy); + } + else + { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx, sy, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, sx, sy, 0, 0); + } + return num_vertices; } STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) { - stbtt_int16 numberOfContours; - stbtt_uint8 *endPtsOfContours; - stbtt_uint8 *data = info->data; - stbtt_vertex *vertices=0; - int num_vertices=0; - int g = stbtt__GetGlyfOffset(info, glyph_index); - - *pvertices = NULL; - - if (g < 0) return 0; - - numberOfContours = ttSHORT(data + g); - - if (numberOfContours > 0) { - stbtt_uint8 flags=0,flagcount; - stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; - stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; - stbtt_uint8 *points; - endPtsOfContours = (data + g + 10); - ins = ttUSHORT(data + g + 10 + numberOfContours * 2); - points = data + g + 10 + numberOfContours * 2 + 2 + ins; - - n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); - - m = n + 2*numberOfContours; // a loose bound on how many vertices we might need - vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); - if (vertices == 0) - return 0; - - next_move = 0; - flagcount=0; - - // in first pass, we load uninterpreted data into the allocated array - // above, shifted to the end of the array so we won't overwrite it when - // we create our final data starting from the front - - off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated - - // first load flags - - for (i=0; i < n; ++i) { - if (flagcount == 0) { - flags = *points++; - if (flags & 8) - flagcount = *points++; - } else - --flagcount; - vertices[off+i].type = flags; - } - - // now load x coordinates - x=0; - for (i=0; i < n; ++i) { - flags = vertices[off+i].type; - if (flags & 2) { - stbtt_int16 dx = *points++; - x += (flags & 16) ? dx : -dx; // ??? - } else { - if (!(flags & 16)) { - x = x + (stbtt_int16) (points[0]*256 + points[1]); - points += 2; - } - } - vertices[off+i].x = (stbtt_int16) x; - } - - // now load y coordinates - y=0; - for (i=0; i < n; ++i) { - flags = vertices[off+i].type; - if (flags & 4) { - stbtt_int16 dy = *points++; - y += (flags & 32) ? dy : -dy; // ??? - } else { - if (!(flags & 32)) { - y = y + (stbtt_int16) (points[0]*256 + points[1]); - points += 2; - } - } - vertices[off+i].y = (stbtt_int16) y; - } - - // now convert them to our format - num_vertices=0; - sx = sy = cx = cy = scx = scy = 0; - for (i=0; i < n; ++i) { - flags = vertices[off+i].type; - x = (stbtt_int16) vertices[off+i].x; - y = (stbtt_int16) vertices[off+i].y; - - if (next_move == i) { - if (i != 0) - num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); - - // now start the new one - start_off = !(flags & 1); - if (start_off) { - // if we start off with an off-curve point, then when we need to find a point on the curve - // where we can start, and we need to save some state for when we wraparound. - scx = x; - scy = y; - if (!(vertices[off+i+1].type & 1)) { - // next point is also a curve point, so interpolate an on-point curve - sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; - sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; - } else { - // otherwise just use the next point as our start point - sx = (stbtt_int32) vertices[off+i+1].x; - sy = (stbtt_int32) vertices[off+i+1].y; - ++i; // we're using point i+1 as the starting point, so skip it - } - } else { - sx = x; - sy = y; - } - stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); - was_off = 0; - next_move = 1 + ttUSHORT(endPtsOfContours+j*2); - ++j; - } else { - if (!(flags & 1)) { // if it's a curve - if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint - stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); - cx = x; - cy = y; - was_off = 1; - } else { - if (was_off) - stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); - else - stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); - was_off = 0; - } - } - } - num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); - } else if (numberOfContours == -1) { - // Compound shapes. - int more = 1; - stbtt_uint8 *comp = data + g + 10; - num_vertices = 0; - vertices = 0; - while (more) { - stbtt_uint16 flags, gidx; - int comp_num_verts = 0, i; - stbtt_vertex *comp_verts = 0, *tmp = 0; - float mtx[6] = {1,0,0,1,0,0}, m, n; - - flags = ttSHORT(comp); comp+=2; - gidx = ttSHORT(comp); comp+=2; - - if (flags & 2) { // XY values - if (flags & 1) { // shorts - mtx[4] = ttSHORT(comp); comp+=2; - mtx[5] = ttSHORT(comp); comp+=2; - } else { - mtx[4] = ttCHAR(comp); comp+=1; - mtx[5] = ttCHAR(comp); comp+=1; - } - } - else { - // @TODO handle matching point - STBTT_assert(0); - } - if (flags & (1<<3)) { // WE_HAVE_A_SCALE - mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; - mtx[1] = mtx[2] = 0; - } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE - mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; - mtx[1] = mtx[2] = 0; - mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; - } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO - mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; - mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; - mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; - mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; - } - - // Find transformation scales. - m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); - n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); - - // Get indexed glyph. - comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); - if (comp_num_verts > 0) { - // Transform vertices. - for (i = 0; i < comp_num_verts; ++i) { - stbtt_vertex* v = &comp_verts[i]; - stbtt_vertex_type x,y; - x=v->x; y=v->y; - v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); - v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); - x=v->cx; y=v->cy; - v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); - v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); - } - // Append vertices. - tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); - if (!tmp) { - if (vertices) STBTT_free(vertices, info->userdata); - if (comp_verts) STBTT_free(comp_verts, info->userdata); - return 0; - } - if (num_vertices > 0) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); - STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); - if (vertices) STBTT_free(vertices, info->userdata); - vertices = tmp; - STBTT_free(comp_verts, info->userdata); - num_vertices += comp_num_verts; - } - // More components ? - more = flags & (1<<5); - } - } else if (numberOfContours < 0) { - // @TODO other compound variations? - STBTT_assert(0); - } else { - // numberOfCounters == 0, do nothing - } - - *pvertices = vertices; - return num_vertices; + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices = 0; + int num_vertices = 0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) + return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) + { + stbtt_uint8 flags = 0, flagcount; + stbtt_int32 ins, i, j = 0, m, n, next_move, was_off = 0, off, start_off = 0; + stbtt_int32 x, y, cx, cy, sx, sy, scx, scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1 + ttUSHORT(endPtsOfContours + numberOfContours * 2 - 2); + + m = n + 2 * numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount = 0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i = 0; i < n; ++i) + { + if (flagcount == 0) + { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } + else + --flagcount; + vertices[off + i].type = flags; + } + + // now load x coordinates + x = 0; + for (i = 0; i < n; ++i) + { + flags = vertices[off + i].type; + if (flags & 2) + { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } + else + { + if (!(flags & 16)) + { + x = x + (stbtt_int16) (points[0] * 256 + points[1]); + points += 2; + } + } + vertices[off + i].x = (stbtt_int16) x; + } + + // now load y coordinates + y = 0; + for (i = 0; i < n; ++i) + { + flags = vertices[off + i].type; + if (flags & 4) + { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } + else + { + if (!(flags & 32)) + { + y = y + (stbtt_int16) (points[0] * 256 + points[1]); + points += 2; + } + } + vertices[off + i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices = 0; + sx = sy = cx = cy = scx = scy = 0; + for (i = 0; i < n; ++i) + { + flags = vertices[off + i].type; + x = (stbtt_int16) vertices[off + i].x; + y = (stbtt_int16) vertices[off + i].y; + + if (next_move == i) + { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx, sy, scx, scy, cx, cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) + { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off + i + 1].type & 1)) + { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off + i + 1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off + i + 1].y) >> 1; + } + else + { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off + i + 1].x; + sy = (stbtt_int32) vertices[off + i + 1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } + else + { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove, sx, sy, 0, 0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours + j * 2); + ++j; + } + else + { + if (!(flags & 1)) + { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx + x) >> 1, (cy + y) >> 1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } + else + { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x, y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x, y, 0, 0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx, sy, scx, scy, cx, cy); + } + else if (numberOfContours == -1) + { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) + { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1, 0, 0, 1, 0, 0}, m, n; + + flags = ttSHORT(comp); + comp += 2; + gidx = ttSHORT(comp); + comp += 2; + + if (flags & 2) + { // XY values + if (flags & 1) + { // shorts + mtx[4] = ttSHORT(comp); + comp += 2; + mtx[5] = ttSHORT(comp); + comp += 2; + } + else + { + mtx[4] = ttCHAR(comp); + comp += 1; + mtx[5] = ttCHAR(comp); + comp += 1; + } + } + else + { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1 << 3)) + { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp) / 16384.0f; + comp += 2; + mtx[1] = mtx[2] = 0; + } + else if (flags & (1 << 6)) + { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp) / 16384.0f; + comp += 2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp) / 16384.0f; + comp += 2; + } + else if (flags & (1 << 7)) + { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp) / 16384.0f; + comp += 2; + mtx[1] = ttSHORT(comp) / 16384.0f; + comp += 2; + mtx[2] = ttSHORT(comp) / 16384.0f; + comp += 2; + mtx[3] = ttSHORT(comp) / 16384.0f; + comp += 2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1]); + n = (float) STBTT_sqrt(mtx[2] * mtx[2] + mtx[3] * mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) + { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) + { + stbtt_vertex *v = &comp_verts[i]; + stbtt_vertex_type x, y; + x = v->x; + y = v->y; + v->x = (stbtt_vertex_type) (m * (mtx[0] * x + mtx[2] * y + mtx[4])); + v->y = (stbtt_vertex_type) (n * (mtx[1] * x + mtx[3] * y + mtx[5])); + x = v->cx; + y = v->cy; + v->cx = (stbtt_vertex_type) (m * (mtx[0] * x + mtx[2] * y + mtx[4])); + v->cy = (stbtt_vertex_type) (n * (mtx[1] * x + mtx[3] * y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex *) STBTT_malloc((num_vertices + comp_num_verts) * sizeof(stbtt_vertex), info->userdata); + if (!tmp) + { + if (vertices) + STBTT_free(vertices, info->userdata); + if (comp_verts) + STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0) + STBTT_memcpy(tmp, vertices, num_vertices * sizeof(stbtt_vertex)); + STBTT_memcpy(tmp + num_vertices, comp_verts, comp_num_verts * sizeof(stbtt_vertex)); + if (vertices) + STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1 << 5); + } + } + else if (numberOfContours < 0) + { + // @TODO other compound variations? + STBTT_assert(0); + } + else + { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; } STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) { - stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); - if (glyph_index < numOfLongHorMetrics) { - if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); - if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); - } else { - if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); - if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); - } -} - -STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) -{ - stbtt_uint8 *data = info->data + info->kern; - stbtt_uint32 needle, straw; - int l, r, m; - - // we only look at the first table. it must be 'horizontal' and format 0. - if (!info->kern) - return 0; - if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 - return 0; - if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format - return 0; - - l = 0; - r = ttUSHORT(data+10) - 1; - needle = glyph1 << 16 | glyph2; - while (l <= r) { - m = (l + r) >> 1; - straw = ttULONG(data+18+(m*6)); // note: unaligned read - if (needle < straw) - r = m - 1; - else if (needle > straw) - l = m + 1; - else - return ttSHORT(data+22+(m*6)); - } - return 0; -} - -STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) -{ - if (!info->kern) // if no kerning table, don't waste time looking up both codepoint->glyphs - return 0; - return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data + info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) + { + if (advanceWidth) + *advanceWidth = ttSHORT(info->data + info->hmtx + 4 * glyph_index); + if (leftSideBearing) + *leftSideBearing = ttSHORT(info->data + info->hmtx + 4 * glyph_index + 2); + } + else + { + if (advanceWidth) + *advanceWidth = ttSHORT(info->data + info->hmtx + 4 * (numOfLongHorMetrics - 1)); + if (leftSideBearing) + *leftSideBearing = ttSHORT(info->data + info->hmtx + 4 * numOfLongHorMetrics + 2 * (glyph_index - numOfLongHorMetrics)); + } +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data + 2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data + 8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data + 10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) + { + m = (l + r) >> 1; + straw = ttULONG(data + 18 + (m * 6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data + 22 + (m * 6)); + } + return 0; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info, ch1), stbtt_FindGlyphIndex(info, ch2)); } STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) { - stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info, codepoint), advanceWidth, leftSideBearing); } STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) { - if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); - if (descent) *descent = ttSHORT(info->data+info->hhea + 6); - if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); + if (ascent) + *ascent = ttSHORT(info->data + info->hhea + 4); + if (descent) + *descent = ttSHORT(info->data + info->hhea + 6); + if (lineGap) + *lineGap = ttSHORT(info->data + info->hhea + 8); } STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) { - *x0 = ttSHORT(info->data + info->head + 36); - *y0 = ttSHORT(info->data + info->head + 38); - *x1 = ttSHORT(info->data + info->head + 40); - *y1 = ttSHORT(info->data + info->head + 42); + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); } STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) { - int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); - return (float) height / fheight; + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; } STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) { - int unitsPerEm = ttUSHORT(info->data + info->head + 18); - return pixels / unitsPerEm; + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; } STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) { - STBTT_free(v, info->userdata); + STBTT_free(v, info->userdata); } ////////////////////////////////////////////////////////////////////////////// @@ -1564,37 +1708,48 @@ STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) // antialiasing software rasterizer // -STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) -{ - int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning - if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { - // e.g. space character - if (ix0) *ix0 = 0; - if (iy0) *iy0 = 0; - if (ix1) *ix1 = 0; - if (iy1) *iy1 = 0; - } else { - // move to integral bboxes (treating pixels as little squares, what pixels get touched)? - if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); - if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); - if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); - if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); - } +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0 = 0, y0 = 0, x1, y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0, &y0, &x1, &y1)) + { + // e.g. space character + if (ix0) + *ix0 = 0; + if (iy0) + *iy0 = 0; + if (ix1) + *ix1 = 0; + if (iy1) + *iy1 = 0; + } + else + { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) + *ix0 = STBTT_ifloor(x0 * scale_x + shift_x); + if (iy0) + *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) + *ix1 = STBTT_iceil(x1 * scale_x + shift_x); + if (iy1) + *iy1 = STBTT_iceil(-y0 * scale_y + shift_y); + } } STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) { - stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y, 0.0f, 0.0f, ix0, iy0, ix1, iy1); } STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) { - stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font, codepoint), scale_x, scale_y, shift_x, shift_y, ix0, iy0, ix1, iy1); } STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) { - stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y, 0.0f, 0.0f, ix0, iy0, ix1, iy1); } ////////////////////////////////////////////////////////////////////////////// @@ -1603,919 +1758,1046 @@ STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codep typedef struct stbtt__hheap_chunk { - struct stbtt__hheap_chunk *next; + struct stbtt__hheap_chunk *next; } stbtt__hheap_chunk; typedef struct stbtt__hheap { - struct stbtt__hheap_chunk *head; - void *first_free; - int num_remaining_in_head_chunk; + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; } stbtt__hheap; static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) { - if (hh->first_free) { - void *p = hh->first_free; - hh->first_free = * (void **) p; - return p; - } else { - if (hh->num_remaining_in_head_chunk == 0) { - int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); - stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); - if (c == NULL) - return NULL; - c->next = hh->head; - hh->head = c; - hh->num_remaining_in_head_chunk = count; - } - --hh->num_remaining_in_head_chunk; - return (char *) (hh->head) + size * hh->num_remaining_in_head_chunk; - } + if (hh->first_free) + { + void *p = hh->first_free; + hh->first_free = *(void **) p; + return p; + } + else + { + if (hh->num_remaining_in_head_chunk == 0) + { + int count = (size < 32 ? 2000 : size < 128 ? 800 : + 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + size * hh->num_remaining_in_head_chunk; + } } static void stbtt__hheap_free(stbtt__hheap *hh, void *p) { - *(void **) p = hh->first_free; - hh->first_free = p; + *(void **) p = hh->first_free; + hh->first_free = p; } static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) { - stbtt__hheap_chunk *c = hh->head; - while (c) { - stbtt__hheap_chunk *n = c->next; - STBTT_free(c, userdata); - c = n; - } + stbtt__hheap_chunk *c = hh->head; + while (c) + { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } } -typedef struct stbtt__edge { - float x0,y0, x1,y1; - int invert; +typedef struct stbtt__edge +{ + float x0, y0, x1, y1; + int invert; } stbtt__edge; - typedef struct stbtt__active_edge { - struct stbtt__active_edge *next; - #if STBTT_RASTERIZER_VERSION==1 - int x,dx; - float ey; - int direction; - #elif STBTT_RASTERIZER_VERSION==2 - float fx,fdx,fdy; - float direction; - float sy; - float ey; - #else - #error "Unrecognized value of STBTT_RASTERIZER_VERSION" - #endif + struct stbtt__active_edge *next; +# if STBTT_RASTERIZER_VERSION == 1 + int x, dx; + float ey; + int direction; +# elif STBTT_RASTERIZER_VERSION == 2 + float fx, fdx, fdy; + float direction; + float sy; + float ey; +# else +# error "Unrecognized value of STBTT_RASTERIZER_VERSION" +# endif } stbtt__active_edge; -#if STBTT_RASTERIZER_VERSION == 1 -#define STBTT_FIXSHIFT 10 -#define STBTT_FIX (1 << STBTT_FIXSHIFT) -#define STBTT_FIXMASK (STBTT_FIX-1) +# if STBTT_RASTERIZER_VERSION == 1 +# define STBTT_FIXSHIFT 10 +# define STBTT_FIX (1 << STBTT_FIXSHIFT) +# define STBTT_FIXMASK (STBTT_FIX - 1) static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) { - stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); - float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); - STBTT_assert(z != NULL); - if (!z) return z; - - // round dx down to avoid overshooting - if (dxdy < 0) - z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); - else - z->dx = STBTT_ifloor(STBTT_FIX * dxdy); - - z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount - z->x -= off_x * STBTT_FIX; - - z->ey = e->y1; - z->next = 0; - z->direction = e->invert ? 1 : -1; - return z; -} -#elif STBTT_RASTERIZER_VERSION == 2 + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) + return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +# elif STBTT_RASTERIZER_VERSION == 2 static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) { - stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); - float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); - STBTT_assert(z != NULL); - //STBTT_assert(e->y0 <= start_point); - if (!z) return z; - z->fdx = dxdy; - z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; - z->fx = e->x0 + dxdy * (start_point - e->y0); - z->fx -= off_x; - z->direction = e->invert ? 1.0f : -1.0f; - z->sy = e->y0; - z->ey = e->y1; - z->next = 0; - return z; -} -#else -#error "Unrecognized value of STBTT_RASTERIZER_VERSION" -#endif - -#if STBTT_RASTERIZER_VERSION == 1 + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + // STBTT_assert(e->y0 <= start_point); + if (!z) + return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f / dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +# else +# error "Unrecognized value of STBTT_RASTERIZER_VERSION" +# endif + +# if STBTT_RASTERIZER_VERSION == 1 // note: this routine clips fills that extend off the edges... ideally this // wouldn't happen, but it could happen if the truetype glyph bounding boxes // are wrong, or if the user supplies a too-small bitmap static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) { - // non-zero winding fill - int x0=0, w=0; - - while (e) { - if (w == 0) { - // if we're currently at zero, we need to record the edge start point - x0 = e->x; w += e->direction; - } else { - int x1 = e->x; w += e->direction; - // if we went to zero, we need to draw - if (w == 0) { - int i = x0 >> STBTT_FIXSHIFT; - int j = x1 >> STBTT_FIXSHIFT; - - if (i < len && j >= 0) { - if (i == j) { - // x0,x1 are the same pixel, so compute combined coverage - scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); - } else { - if (i >= 0) // add antialiasing for x0 - scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); - else - i = -1; // clip - - if (j < len) // add antialiasing for x1 - scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); - else - j = len; // clip - - for (++i; i < j; ++i) // fill pixels between x0 and x1 - scanline[i] = scanline[i] + (stbtt_uint8) max_weight; - } - } - } - } - - e = e->next; - } + // non-zero winding fill + int x0 = 0, w = 0; + + while (e) + { + if (w == 0) + { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; + w += e->direction; + } + else + { + int x1 = e->x; + w += e->direction; + // if we went to zero, we need to draw + if (w == 0) + { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) + { + if (i == j) + { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } + else + { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } } static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) { - stbtt__hheap hh = { 0, 0, 0 }; - stbtt__active_edge *active = NULL; - int y,j=0; - int max_weight = (255 / vsubsample); // weight per vertical scanline - int s; // vertical subsample index - unsigned char scanline_data[512], *scanline; - - if (result->w > 512) - scanline = (unsigned char *) STBTT_malloc(result->w, userdata); - else - scanline = scanline_data; - - y = off_y * vsubsample; - e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; - - while (j < result->h) { - STBTT_memset(scanline, 0, result->w); - for (s=0; s < vsubsample; ++s) { - // find center of pixel for this scanline - float scan_y = y + 0.5f; - stbtt__active_edge **step = &active; - - // update all active edges; - // remove all active edges that terminate before the center of this scanline - while (*step) { - stbtt__active_edge * z = *step; - if (z->ey <= scan_y) { - *step = z->next; // delete from list - STBTT_assert(z->direction); - z->direction = 0; - stbtt__hheap_free(&hh, z); - } else { - z->x += z->dx; // advance to position for current scanline - step = &((*step)->next); // advance through list - } - } - - // resort the list if needed - for(;;) { - int changed=0; - step = &active; - while (*step && (*step)->next) { - if ((*step)->x > (*step)->next->x) { - stbtt__active_edge *t = *step; - stbtt__active_edge *q = t->next; - - t->next = q->next; - q->next = t; - *step = q; - changed = 1; - } - step = &(*step)->next; - } - if (!changed) break; - } - - // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline - while (e->y0 <= scan_y) { - if (e->y1 > scan_y) { - stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); - if (z != NULL) { - // find insertion point - if (active == NULL) - active = z; - else if (z->x < active->x) { - // insert at front - z->next = active; - active = z; - } else { - // find thing to insert AFTER - stbtt__active_edge *p = active; - while (p->next && p->next->x < z->x) - p = p->next; - // at this point, p->next->x is NOT < z->x - z->next = p->next; - p->next = z; - } - } - } - ++e; - } - - // now process all active edges in XOR fashion - if (active) - stbtt__fill_active_edges(scanline, result->w, active, max_weight); - - ++y; - } - STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); - ++j; - } - - stbtt__hheap_cleanup(&hh, userdata); - - if (scanline != scanline_data) - STBTT_free(scanline, userdata); -} - -#elif STBTT_RASTERIZER_VERSION == 2 + stbtt__hheap hh = {0, 0, 0}; + stbtt__active_edge *active = NULL; + int y, j = 0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) + { + STBTT_memset(scanline, 0, result->w); + for (s = 0; s < vsubsample; ++s) + { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) + { + stbtt__active_edge *z = *step; + if (z->ey <= scan_y) + { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } + else + { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for (;;) + { + int changed = 0; + step = &active; + while (*step && (*step)->next) + { + if ((*step)->x > (*step)->next->x) + { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) + break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) + { + if (e->y1 > scan_y) + { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) + { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) + { + // insert at front + z->next = active; + active = z; + } + else + { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +# elif STBTT_RASTERIZER_VERSION == 2 // the edge passed in here does not cross the vertical line at x or the vertical line at x+1 // (i.e. it has already been clipped to those) static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) { - if (y0 == y1) return; - STBTT_assert(y0 < y1); - STBTT_assert(e->sy <= e->ey); - if (y0 > e->ey) return; - if (y1 < e->sy) return; - if (y0 < e->sy) { - x0 += (x1-x0) * (e->sy - y0) / (y1-y0); - y0 = e->sy; - } - if (y1 > e->ey) { - x1 += (x1-x0) * (e->ey - y1) / (y1-y0); - y1 = e->ey; - } - - if (x0 == x) - STBTT_assert(x1 <= x+1); - else if (x0 == x+1) - STBTT_assert(x1 >= x); - else if (x0 <= x) - STBTT_assert(x1 <= x); - else if (x0 >= x+1) - STBTT_assert(x1 >= x+1); - else - STBTT_assert(x1 >= x && x1 <= x+1); - - if (x0 <= x && x1 <= x) - scanline[x] += e->direction * (y1-y0); - else if (x0 >= x+1 && x1 >= x+1) - ; - else { - STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); - scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position - } + if (y0 == y1) + return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) + return; + if (y1 < e->sy) + return; + if (y0 < e->sy) + { + x0 += (x1 - x0) * (e->sy - y0) / (y1 - y0); + y0 = e->sy; + } + if (y1 > e->ey) + { + x1 += (x1 - x0) * (e->ey - y1) / (y1 - y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x + 1); + else if (x0 == x + 1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x + 1) + STBTT_assert(x1 >= x + 1); + else + STBTT_assert(x1 >= x && x1 <= x + 1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1 - y0); + else if (x0 >= x + 1 && x1 >= x + 1) + ; + else + { + STBTT_assert(x0 >= x && x0 <= x + 1 && x1 >= x && x1 <= x + 1); + scanline[x] += e->direction * (y1 - y0) * (1 - ((x0 - x) + (x1 - x)) / 2); // coverage = 1 - average x position + } } static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) { - float y_bottom = y_top+1; - - while (e) { - // brute force every pixel - - // compute intersection points with top & bottom - STBTT_assert(e->ey >= y_top); - - if (e->fdx == 0) { - float x0 = e->fx; - if (x0 < len) { - if (x0 >= 0) { - stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); - stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); - } else { - stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); - } - } - } else { - float x0 = e->fx; - float dx = e->fdx; - float xb = x0 + dx; - float x_top, x_bottom; - float sy0,sy1; - float dy = e->fdy; - STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); - - // compute endpoints of line segment clipped to this scanline (if the - // line segment starts on this scanline. x0 is the intersection of the - // line with y_top, but that may be off the line segment. - if (e->sy > y_top) { - x_top = x0 + dx * (e->sy - y_top); - sy0 = e->sy; - } else { - x_top = x0; - sy0 = y_top; - } - if (e->ey < y_bottom) { - x_bottom = x0 + dx * (e->ey - y_top); - sy1 = e->ey; - } else { - x_bottom = xb; - sy1 = y_bottom; - } - - if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { - // from here on, we don't have to range check x values - - if ((int) x_top == (int) x_bottom) { - float height; - // simple case, only spans one pixel - int x = (int) x_top; - height = sy1 - sy0; - STBTT_assert(x >= 0 && x < len); - scanline[x] += e->direction * (1-((x_top - x) + (x_bottom-x))/2) * height; - scanline_fill[x] += e->direction * height; // everything right of this pixel is filled - } else { - int x,x1,x2; - float y_crossing, step, sign, area; - // covers 2+ pixels - if (x_top > x_bottom) { - // flip scanline vertically; signed area is the same - float t; - sy0 = y_bottom - (sy0 - y_top); - sy1 = y_bottom - (sy1 - y_top); - t = sy0, sy0 = sy1, sy1 = t; - t = x_bottom, x_bottom = x_top, x_top = t; - dx = -dx; - dy = -dy; - t = x0, x0 = xb, xb = t; - } - - x1 = (int) x_top; - x2 = (int) x_bottom; - // compute intersection with y axis at x1+1 - y_crossing = (x1+1 - x0) * dy + y_top; - - sign = e->direction; - // area of the rectangle covered from y0..y_crossing - area = sign * (y_crossing-sy0); - // area of the triangle (x_top,y0), (x+1,y0), (x+1,y_crossing) - scanline[x1] += area * (1-((x_top - x1)+(x1+1-x1))/2); - - step = sign * dy; - for (x = x1+1; x < x2; ++x) { - scanline[x] += area + step/2; - area += step; - } - y_crossing += dy * (x2 - (x1+1)); - - STBTT_assert(STBTT_fabs(area) <= 1.01f); - - scanline[x2] += area + sign * (1-((x2-x2)+(x_bottom-x2))/2) * (sy1-y_crossing); - - scanline_fill[x2] += sign * (sy1-sy0); - } - } else { - // if edge goes outside of box we're drawing, we require - // clipping logic. since this does not match the intended use - // of this library, we use a different, very slow brute - // force implementation - int x; - for (x=0; x < len; ++x) { - // cases: - // - // there can be up to two intersections with the pixel. any intersection - // with left or right edges can be handled by splitting into two (or three) - // regions. intersections with top & bottom do not necessitate case-wise logic. - // - // the old way of doing this found the intersections with the left & right edges, - // then used some simple logic to produce up to three segments in sorted order - // from top-to-bottom. however, this had a problem: if an x edge was epsilon - // across the x border, then the corresponding y position might not be distinct - // from the other y segment, and it might ignored as an empty segment. to avoid - // that, we need to explicitly produce segments based on x positions. - - // rename variables to clear pairs - float y0 = y_top; - float x1 = (float) (x); - float x2 = (float) (x+1); - float x3 = xb; - float y3 = y_bottom; - float y1,y2; - - // x = e->x + e->dx * (y-y_top) - // (y-y_top) = (x - e->x) / e->dx - // y = (x - e->x) / e->dx + y_top - y1 = (x - x0) / dx + y_top; - y2 = (x+1 - x0) / dx + y_top; - - if (x0 < x1 && x3 > x2) { // three segments descending down-right - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); - stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); - stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); - } else if (x3 < x1 && x0 > x2) { // three segments descending down-left - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); - stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); - stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); - } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); - stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); - } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); - stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); - } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); - stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); - } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); - stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); - } else { // one segment - stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); - } - } - } - } - e = e->next; - } + float y_bottom = y_top + 1; + + while (e) + { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) + { + float x0 = e->fx; + if (x0 < len) + { + if (x0 >= 0) + { + stbtt__handle_clipped_edge(scanline, (int) x0, e, x0, y_top, x0, y_bottom); + stbtt__handle_clipped_edge(scanline_fill - 1, (int) x0 + 1, e, x0, y_top, x0, y_bottom); + } + else + { + stbtt__handle_clipped_edge(scanline_fill - 1, 0, e, x0, y_top, x0, y_bottom); + } + } + } + else + { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0, sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) + { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } + else + { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) + { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } + else + { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) + { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) + { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = sy1 - sy0; + STBTT_assert(x >= 0 && x < len); + scanline[x] += e->direction * (1 - ((x_top - x) + (x_bottom - x)) / 2) * height; + scanline_fill[x] += e->direction * height; // everything right of this pixel is filled + } + else + { + int x, x1, x2; + float y_crossing, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) + { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + } + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = (x1 + 1 - x0) * dy + y_top; + + sign = e->direction; + // area of the rectangle covered from y0..y_crossing + area = sign * (y_crossing - sy0); + // area of the triangle (x_top,y0), (x+1,y0), (x+1,y_crossing) + scanline[x1] += area * (1 - ((x_top - x1) + (x1 + 1 - x1)) / 2); + + step = sign * dy; + for (x = x1 + 1; x < x2; ++x) + { + scanline[x] += area + step / 2; + area += step; + } + y_crossing += dy * (x2 - (x1 + 1)); + + STBTT_assert(STBTT_fabs(area) <= 1.01f); + + scanline[x2] += area + sign * (1 - ((x2 - x2) + (x_bottom - x2)) / 2) * (sy1 - y_crossing); + + scanline_fill[x2] += sign * (sy1 - sy0); + } + } + else + { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + int x; + for (x = 0; x < len; ++x) + { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clear pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x + 1); + float x3 = xb; + float y3 = y_bottom; + float y1, y2; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + y1 = (x - x0) / dx + y_top; + y2 = (x + 1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) + { // three segments descending down-right + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x1, y1); + stbtt__handle_clipped_edge(scanline, x, e, x1, y1, x2, y2); + stbtt__handle_clipped_edge(scanline, x, e, x2, y2, x3, y3); + } + else if (x3 < x1 && x0 > x2) + { // three segments descending down-left + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x2, y2); + stbtt__handle_clipped_edge(scanline, x, e, x2, y2, x1, y1); + stbtt__handle_clipped_edge(scanline, x, e, x1, y1, x3, y3); + } + else if (x0 < x1 && x3 > x1) + { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x1, y1); + stbtt__handle_clipped_edge(scanline, x, e, x1, y1, x3, y3); + } + else if (x3 < x1 && x0 > x1) + { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x1, y1); + stbtt__handle_clipped_edge(scanline, x, e, x1, y1, x3, y3); + } + else if (x0 < x2 && x3 > x2) + { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x2, y2); + stbtt__handle_clipped_edge(scanline, x, e, x2, y2, x3, y3); + } + else if (x3 < x2 && x0 > x2) + { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x2, y2); + stbtt__handle_clipped_edge(scanline, x, e, x2, y2, x3, y3); + } + else + { // one segment + stbtt__handle_clipped_edge(scanline, x, e, x0, y0, x3, y3); + } + } + } + } + e = e->next; + } } // directly AA rasterize edges w/o supersampling static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) { - (void)vsubsample; - stbtt__hheap hh = { 0, 0, 0 }; - stbtt__active_edge *active = NULL; - int y,j=0, i; - float scanline_data[129], *scanline, *scanline2; - - if (result->w > 64) - scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); - else - scanline = scanline_data; - - scanline2 = scanline + result->w; - - y = off_y; - e[n].y0 = (float) (off_y + result->h) + 1; - - while (j < result->h) { - // find center of pixel for this scanline - float scan_y_top = y + 0.0f; - float scan_y_bottom = y + 1.0f; - stbtt__active_edge **step = &active; - - STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); - STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); - - // update all active edges; - // remove all active edges that terminate before the top of this scanline - while (*step) { - stbtt__active_edge * z = *step; - if (z->ey <= scan_y_top) { - *step = z->next; // delete from list - STBTT_assert(z->direction); - z->direction = 0; - stbtt__hheap_free(&hh, z); - } else { - step = &((*step)->next); // advance through list - } - } - - // insert all edges that start before the bottom of this scanline - while (e->y0 <= scan_y_bottom) { - if (e->y0 != e->y1) { - stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); - if (z != NULL) { - STBTT_assert(z->ey >= scan_y_top); - // insert at front - z->next = active; - active = z; - } - } - ++e; - } - - // now process all active edges - if (active) - stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); - - { - float sum = 0; - for (i=0; i < result->w; ++i) { - float k; - int m; - sum += scanline2[i]; - k = scanline[i] + sum; - k = (float) STBTT_fabs(k)*255 + 0.5f; - m = (int) k; - if (m > 255) m = 255; - result->pixels[j*result->stride + i] = (unsigned char) m; - } - } - // advance all the edges - step = &active; - while (*step) { - stbtt__active_edge *z = *step; - z->fx += z->fdx; // advance to position for current scanline - step = &((*step)->next); // advance through list - } - - ++y; - ++j; - } - - stbtt__hheap_cleanup(&hh, userdata); - - if (scanline != scanline_data) - STBTT_free(scanline, userdata); -} -#else -#error "Unrecognized value of STBTT_RASTERIZER_VERSION" -#endif - -#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + (void) vsubsample; + stbtt__hheap hh = {0, 0, 0}; + stbtt__active_edge *active = NULL; + int y, j = 0, i; + float scanline_data[129], *scanline, *scanline2; + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w * 2 + 1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) + { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline, 0, result->w * sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w + 1) * sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) + { + stbtt__active_edge *z = *step; + if (z->ey <= scan_y_top) + { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } + else + { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) + { + if (e->y0 != e->y1) + { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) + { + STBTT_assert(z->ey >= scan_y_top); + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2 + 1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i = 0; i < result->w; ++i) + { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k) * 255 + 0.5f; + m = (int) k; + if (m > 255) + m = 255; + result->pixels[j * result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) + { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +# else +# error "Unrecognized value of STBTT_RASTERIZER_VERSION" +# endif + +# define STBTT__COMPARE(a, b) ((a)->y0 < (b)->y0) static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) { - int i,j; - for (i=1; i < n; ++i) { - stbtt__edge t = p[i], *a = &t; - j = i; - while (j > 0) { - stbtt__edge *b = &p[j-1]; - int c = STBTT__COMPARE(a,b); - if (!c) break; - p[j] = p[j-1]; - --j; - } - if (i != j) - p[j] = t; - } + int i, j; + for (i = 1; i < n; ++i) + { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) + { + stbtt__edge *b = &p[j - 1]; + int c = STBTT__COMPARE(a, b); + if (!c) + break; + p[j] = p[j - 1]; + --j; + } + if (i != j) + p[j] = t; + } } static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) { - /* threshhold for transitioning to insertion sort */ - while (n > 12) { - stbtt__edge t; - int c01,c12,c,m,i,j; - - /* compute median of three */ - m = n >> 1; - c01 = STBTT__COMPARE(&p[0],&p[m]); - c12 = STBTT__COMPARE(&p[m],&p[n-1]); - /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ - if (c01 != c12) { - /* otherwise, we'll need to swap something else to middle */ - int z; - c = STBTT__COMPARE(&p[0],&p[n-1]); - /* 0>mid && midn => n; 0 0 */ - /* 0n: 0>n => 0; 0 n */ - z = (c == c12) ? 0 : n-1; - t = p[z]; - p[z] = p[m]; - p[m] = t; - } - /* now p[m] is the median-of-three */ - /* swap it to the beginning so it won't move around */ - t = p[0]; - p[0] = p[m]; - p[m] = t; - - /* partition loop */ - i=1; - j=n-1; - for(;;) { - /* handling of equality is crucial here */ - /* for sentinels & efficiency with duplicates */ - for (;;++i) { - if (!STBTT__COMPARE(&p[i], &p[0])) break; - } - for (;;--j) { - if (!STBTT__COMPARE(&p[0], &p[j])) break; - } - /* make sure we haven't crossed */ - if (i >= j) break; - t = p[i]; - p[i] = p[j]; - p[j] = t; - - ++i; - --j; - } - /* recurse on smaller side, iterate on larger */ - if (j < (n-i)) { - stbtt__sort_edges_quicksort(p,j); - p = p+i; - n = n-i; - } else { - stbtt__sort_edges_quicksort(p+i, n-i); - n = j; - } - } + /* threshhold for transitioning to insertion sort */ + while (n > 12) + { + stbtt__edge t; + int c01, c12, c, m, i, j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0], &p[m]); + c12 = STBTT__COMPARE(&p[m], &p[n - 1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) + { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0], &p[n - 1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n - 1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i = 1; + j = n - 1; + for (;;) + { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;; ++i) + { + if (!STBTT__COMPARE(&p[i], &p[0])) + break; + } + for (;; --j) + { + if (!STBTT__COMPARE(&p[0], &p[j])) + break; + } + /* make sure we haven't crossed */ + if (i >= j) + break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n - i)) + { + stbtt__sort_edges_quicksort(p, j); + p = p + i; + n = n - i; + } + else + { + stbtt__sort_edges_quicksort(p + i, n - i); + n = j; + } + } } static void stbtt__sort_edges(stbtt__edge *p, int n) { - stbtt__sort_edges_quicksort(p, n); - stbtt__sort_edges_ins_sort(p, n); + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); } typedef struct { - float x,y; + float x, y; } stbtt__point; static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) { - float y_scale_inv = invert ? -scale_y : scale_y; - stbtt__edge *e; - int n,i,j,k,m; -#if STBTT_RASTERIZER_VERSION == 1 - int vsubsample = result->h < 8 ? 15 : 5; -#elif STBTT_RASTERIZER_VERSION == 2 - int vsubsample = 1; -#else - #error "Unrecognized value of STBTT_RASTERIZER_VERSION" -#endif - // vsubsample should divide 255 evenly; otherwise we won't reach full opacity - - // now we have to blow out the windings into explicit edge lists - n = 0; - for (i=0; i < windings; ++i) - n += wcount[i]; - - e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel - if (e == 0) return; - n = 0; - - m=0; - for (i=0; i < windings; ++i) { - stbtt__point *p = pts + m; - m += wcount[i]; - j = wcount[i]-1; - for (k=0; k < wcount[i]; j=k++) { - int a=k,b=j; - // skip the edge if horizontal - if (p[j].y == p[k].y) - continue; - // add edge from j to k to the list - e[n].invert = 0; - if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { - e[n].invert = 1; - a=j,b=k; - } - e[n].x0 = p[a].x * scale_x + shift_x; - e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; - e[n].x1 = p[b].x * scale_x + shift_x; - e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; - ++n; - } - } - - // now sort the edges by their highest point (should snap to integer, and then by x) - //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); - stbtt__sort_edges(e, n); - - // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule - stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); - - STBTT_free(e, userdata); + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n, i, j, k, m; +# if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +# elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +# else +# error "Unrecognized value of STBTT_RASTERIZER_VERSION" +# endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i = 0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n + 1), userdata); // add an extra one as a sentinel + if (e == 0) + return; + n = 0; + + m = 0; + for (i = 0; i < windings; ++i) + { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i] - 1; + for (k = 0; k < wcount[i]; j = k++) + { + int a = k, b = j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) + { + e[n].invert = 1; + a = j, b = k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + // STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); } static void stbtt__add_point(stbtt__point *points, int n, float x, float y) { - if (!points) return; // during first pass, it's unallocated - points[n].x = x; - points[n].y = y; + if (!points) + return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; } // tesselate until threshhold p is happy... @TODO warped to compensate for non-linear stretching static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) { - // midpoint - float mx = (x0 + 2*x1 + x2)/4; - float my = (y0 + 2*y1 + y2)/4; - // versus directly drawn line - float dx = (x0+x2)/2 - mx; - float dy = (y0+y2)/2 - my; - if (n > 16) // 65536 segments on one curve better be enough! - return 1; - if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA - stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); - stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); - } else { - stbtt__add_point(points, *num_points,x2,y2); - *num_points = *num_points+1; - } - return 1; + // midpoint + float mx = (x0 + 2 * x1 + x2) / 4; + float my = (y0 + 2 * y1 + y2) / 4; + // versus directly drawn line + float dx = (x0 + x2) / 2 - mx; + float dy = (y0 + y2) / 2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx * dx + dy * dy > objspace_flatness_squared) + { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0, y0, (x0 + x1) / 2.0f, (y0 + y1) / 2.0f, mx, my, objspace_flatness_squared, n + 1); + stbtt__tesselate_curve(points, num_points, mx, my, (x1 + x2) / 2.0f, (y1 + y2) / 2.0f, x2, y2, objspace_flatness_squared, n + 1); + } + else + { + stbtt__add_point(points, *num_points, x2, y2); + *num_points = *num_points + 1; + } + return 1; } // returns number of contours static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) { - stbtt__point *points=0; - int num_points=0; - - float objspace_flatness_squared = objspace_flatness * objspace_flatness; - int i,n=0,start=0, pass; - - // count how many "moves" there are to get the contour count - for (i=0; i < num_verts; ++i) - if (vertices[i].type == STBTT_vmove) - ++n; - - *num_contours = n; - if (n == 0) return 0; - - *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); - - if (*contour_lengths == 0) { - *num_contours = 0; - return 0; - } - - // make two passes through the points so we don't need to realloc - for (pass=0; pass < 2; ++pass) { - float x=0,y=0; - if (pass == 1) { - points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); - if (points == NULL) goto error; - } - num_points = 0; - n= -1; - for (i=0; i < num_verts; ++i) { - switch (vertices[i].type) { - case STBTT_vmove: - // start the next contour - if (n >= 0) - (*contour_lengths)[n] = num_points - start; - ++n; - start = num_points; - - x = vertices[i].x, y = vertices[i].y; - stbtt__add_point(points, num_points++, x,y); - break; - case STBTT_vline: - x = vertices[i].x, y = vertices[i].y; - stbtt__add_point(points, num_points++, x, y); - break; - case STBTT_vcurve: - stbtt__tesselate_curve(points, &num_points, x,y, - vertices[i].cx, vertices[i].cy, - vertices[i].x, vertices[i].y, - objspace_flatness_squared, 0); - x = vertices[i].x, y = vertices[i].y; - break; - } - } - (*contour_lengths)[n] = num_points - start; - } - - return points; + stbtt__point *points = 0; + int num_points = 0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i, n = 0, start = 0, pass; + + // count how many "moves" there are to get the contour count + for (i = 0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) + return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) + { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass = 0; pass < 2; ++pass) + { + float x = 0, y = 0; + if (pass == 1) + { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) + goto error; + } + num_points = 0; + n = -1; + for (i = 0; i < num_verts; ++i) + { + switch (vertices[i].type) + { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x, y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; error: - STBTT_free(points, userdata); - STBTT_free(*contour_lengths, userdata); - *contour_lengths = 0; - *num_contours = 0; - return NULL; + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; } STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) { - float scale = scale_x > scale_y ? scale_y : scale_x; - int winding_count, *winding_lengths; - stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); - if (windings) { - stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); - STBTT_free(winding_lengths, userdata); - STBTT_free(windings, userdata); - } + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count, *winding_lengths; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) + { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } } STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) { - STBTT_free(bitmap, userdata); + STBTT_free(bitmap, userdata); } STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) { - int ix0,iy0,ix1,iy1; - stbtt__bitmap gbm; - stbtt_vertex *vertices; - int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); - - if (scale_x == 0) scale_x = scale_y; - if (scale_y == 0) { - if (scale_x == 0) { - STBTT_free(vertices, info->userdata); - return NULL; - } - scale_y = scale_x; - } - - stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); - - // now we get the size - gbm.w = (ix1 - ix0); - gbm.h = (iy1 - iy0); - gbm.pixels = NULL; // in case we error - - if (width ) *width = gbm.w; - if (height) *height = gbm.h; - if (xoff ) *xoff = ix0; - if (yoff ) *yoff = iy0; - - if (gbm.w && gbm.h) { - gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); - if (gbm.pixels) { - gbm.stride = gbm.w; - - stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); - } - } - STBTT_free(vertices, info->userdata); - return gbm.pixels; -} + int ix0, iy0, ix1, iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) + scale_x = scale_y; + if (scale_y == 0) + { + if (scale_x == 0) + { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0, &iy0, &ix1, &iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width) + *width = gbm.w; + if (height) + *height = gbm.h; + if (xoff) + *xoff = ix0; + if (yoff) + *yoff = iy0; + + if (gbm.w && gbm.h) + { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) + { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) { - return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); } STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) { - int ix0,iy0; - stbtt_vertex *vertices; - int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); - stbtt__bitmap gbm; + int ix0, iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; - stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); - gbm.pixels = output; - gbm.w = out_w; - gbm.h = out_h; - gbm.stride = out_stride; + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0, &iy0, 0, 0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; - if (gbm.w && gbm.h) - stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); - STBTT_free(vertices, info->userdata); + STBTT_free(vertices, info->userdata); } STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) { - stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f, 0.0f, glyph); } STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { - return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); -} + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info, codepoint), width, height, xoff, yoff); +} STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) { - stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info, codepoint)); } STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { - return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); -} + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, codepoint, width, height, xoff, yoff); +} STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) { - stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f, 0.0f, codepoint); } ////////////////////////////////////////////////////////////////////////////// @@ -2524,71 +2806,72 @@ STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned ch // // This is SUPER-CRAPPY packing to keep source code small -STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) - float pixel_height, // height of font in pixels - unsigned char *pixels, int pw, int ph, // bitmap to be filled in - int first_char, int num_chars, // characters to bake - stbtt_bakedchar *chardata) -{ - float scale; - int x,y,bottom_y, i; - stbtt_fontinfo f; - f.userdata = NULL; - if (!stbtt_InitFont(&f, data, offset)) - return -1; - STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels - x=y=1; - bottom_y = 1; - - scale = stbtt_ScaleForPixelHeight(&f, pixel_height); - - for (i=0; i < num_chars; ++i) { - int advance, lsb, x0,y0,x1,y1,gw,gh; - int g = stbtt_FindGlyphIndex(&f, first_char + i); - stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); - stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); - gw = x1-x0; - gh = y1-y0; - if (x + gw + 1 >= pw) - y = bottom_y, x = 1; // advance to next row - if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row - return -i; - STBTT_assert(x+gw < pw); - STBTT_assert(y+gh < ph); - stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); - chardata[i].x0 = (stbtt_int16) x; - chardata[i].y0 = (stbtt_int16) y; - chardata[i].x1 = (stbtt_int16) (x + gw); - chardata[i].y1 = (stbtt_int16) (y + gh); - chardata[i].xadvance = scale * advance; - chardata[i].xoff = (float) x0; - chardata[i].yoff = (float) y0; - x = x + gw + 1; - if (y+gh+1 > bottom_y) - bottom_y = y+gh+1; - } - return bottom_y; +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x, y, bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw * ph); // background of 0 around pixels + x = y = 1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i = 0; i < num_chars; ++i) + { + int advance, lsb, x0, y0, x1, y1, gw, gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale, scale, &x0, &y0, &x1, &y1); + gw = x1 - x0; + gh = y1 - y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x + gw < pw); + STBTT_assert(y + gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels + x + y * pw, gw, gh, pw, scale, scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y + gh + 1 > bottom_y) + bottom_y = y + gh + 1; + } + return bottom_y; } STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) { - float d3d_bias = opengl_fillrule ? 0 : -0.5f; - float ipw = 1.0f / pw, iph = 1.0f / ph; - stbtt_bakedchar *b = chardata + char_index; - int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); - int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); - q->x0 = round_x + d3d_bias; - q->y0 = round_y + d3d_bias; - q->x1 = round_x + b->x1 - b->x0 + d3d_bias; - q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; - q->s0 = b->x0 * ipw; - q->t0 = b->y0 * iph; - q->s1 = b->x1 * ipw; - q->t1 = b->y1 * iph; + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; - *xpos += b->xadvance; + *xpos += b->xadvance; } ////////////////////////////////////////////////////////////////////////////// @@ -2596,12 +2879,12 @@ STBTT_DEF void stbtt_GetBakedQuad(stbtt_bakedchar *chardata, int pw, int ph, int // rectangle packing replacement routines if you don't have stb_rect_pack.h // -#ifndef STB_RECT_PACK_VERSION -#ifdef _MSC_VER -#define STBTT__NOTUSED(v) (void)(v) -#else -#define STBTT__NOTUSED(v) (void)sizeof(v) -#endif +# ifndef STB_RECT_PACK_VERSION +# ifdef _MSC_VER +# define STBTT__NOTUSED(v) (void) (v) +# else +# define STBTT__NOTUSED(v) (void) sizeof(v) +# endif typedef int stbrp_coord; @@ -2618,53 +2901,55 @@ typedef int stbrp_coord; typedef struct { - int width,height; - int x,y,bottom_y; + int width, height; + int x, y, bottom_y; } stbrp_context; typedef struct { - unsigned char x; + unsigned char x; } stbrp_node; struct stbrp_rect { - stbrp_coord x,y; - int id,w,h,was_packed; + stbrp_coord x, y; + int id, w, h, was_packed; }; static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) { - con->width = pw; - con->height = ph; - con->x = 0; - con->y = 0; - con->bottom_y = 0; - STBTT__NOTUSED(nodes); - STBTT__NOTUSED(num_nodes); + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); } static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) { - int i; - for (i=0; i < num_rects; ++i) { - if (con->x + rects[i].w > con->width) { - con->x = 0; - con->y = con->bottom_y; - } - if (con->y + rects[i].h > con->height) - break; - rects[i].x = con->x; - rects[i].y = con->y; - rects[i].was_packed = 1; - con->x += rects[i].w; - if (con->y + rects[i].h > con->bottom_y) - con->bottom_y = con->y + rects[i].h; - } - for ( ; i < num_rects; ++i) - rects[i].was_packed = 0; -} -#endif + int i; + for (i = 0; i < num_rects; ++i) + { + if (con->x + rects[i].w > con->width) + { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for (; i < num_rects; ++i) + rects[i].was_packed = 0; +} +# endif ////////////////////////////////////////////////////////////////////////////// // @@ -2675,544 +2960,622 @@ static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rect STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) { - stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); - int num_nodes = pw - padding; - stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context), alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes) * num_nodes, alloc_context); - if (context == NULL || nodes == NULL) { - if (context != NULL) STBTT_free(context, alloc_context); - if (nodes != NULL) STBTT_free(nodes , alloc_context); - return 0; - } + if (context == NULL || nodes == NULL) + { + if (context != NULL) + STBTT_free(context, alloc_context); + if (nodes != NULL) + STBTT_free(nodes, alloc_context); + return 0; + } - spc->user_allocator_context = alloc_context; - spc->width = pw; - spc->height = ph; - spc->pixels = pixels; - spc->pack_info = context; - spc->nodes = nodes; - spc->padding = padding; - spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; - spc->h_oversample = 1; - spc->v_oversample = 1; + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; - stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + stbrp_init_target(context, pw - padding, ph - padding, nodes, num_nodes); - if (pixels) - STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + if (pixels) + STBTT_memset(pixels, 0, pw * ph); // background of 0 around pixels - return 1; + return 1; } -STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +STBTT_DEF void stbtt_PackEnd(stbtt_pack_context *spc) { - STBTT_free(spc->nodes , spc->user_allocator_context); - STBTT_free(spc->pack_info, spc->user_allocator_context); + STBTT_free(spc->nodes, spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); } STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) { - STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); - STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); - if (h_oversample <= STBTT_MAX_OVERSAMPLE) - spc->h_oversample = h_oversample; - if (v_oversample <= STBTT_MAX_OVERSAMPLE) - spc->v_oversample = v_oversample; + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; } -#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) +# define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE - 1) static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) { - unsigned char buffer[STBTT_MAX_OVERSAMPLE]; - int safe_w = w - kernel_width; - int j; - STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze - for (j=0; j < h; ++j) { - int i; - unsigned int total; - STBTT_memset(buffer, 0, kernel_width); - - total = 0; - - // make kernel_width a constant in common cases so compiler can optimize out the divide - switch (kernel_width) { - case 2: - for (i=0; i <= safe_w; ++i) { - total += pixels[i] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; - pixels[i] = (unsigned char) (total / 2); - } - break; - case 3: - for (i=0; i <= safe_w; ++i) { - total += pixels[i] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; - pixels[i] = (unsigned char) (total / 3); - } - break; - case 4: - for (i=0; i <= safe_w; ++i) { - total += pixels[i] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; - pixels[i] = (unsigned char) (total / 4); - } - break; - case 5: - for (i=0; i <= safe_w; ++i) { - total += pixels[i] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; - pixels[i] = (unsigned char) (total / 5); - } - break; - default: - for (i=0; i <= safe_w; ++i) { - total += pixels[i] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; - pixels[i] = (unsigned char) (total / kernel_width); - } - break; - } - - for (; i < w; ++i) { - STBTT_assert(pixels[i] == 0); - total -= buffer[i & STBTT__OVER_MASK]; - pixels[i] = (unsigned char) (total / kernel_width); - } - - pixels += stride_in_bytes; - } + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j = 0; j < h; ++j) + { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) + { + case 2: + for (i = 0; i <= safe_w; ++i) + { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i = 0; i <= safe_w; ++i) + { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i = 0; i <= safe_w; ++i) + { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i = 0; i <= safe_w; ++i) + { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i = 0; i <= safe_w; ++i) + { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) + { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } } static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) { - unsigned char buffer[STBTT_MAX_OVERSAMPLE]; - int safe_h = h - kernel_width; - int j; - STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze - for (j=0; j < w; ++j) { - int i; - unsigned int total; - STBTT_memset(buffer, 0, kernel_width); - - total = 0; - - // make kernel_width a constant in common cases so compiler can optimize out the divide - switch (kernel_width) { - case 2: - for (i=0; i <= safe_h; ++i) { - total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; - pixels[i*stride_in_bytes] = (unsigned char) (total / 2); - } - break; - case 3: - for (i=0; i <= safe_h; ++i) { - total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; - pixels[i*stride_in_bytes] = (unsigned char) (total / 3); - } - break; - case 4: - for (i=0; i <= safe_h; ++i) { - total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; - pixels[i*stride_in_bytes] = (unsigned char) (total / 4); - } - break; - case 5: - for (i=0; i <= safe_h; ++i) { - total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; - pixels[i*stride_in_bytes] = (unsigned char) (total / 5); - } - break; - default: - for (i=0; i <= safe_h; ++i) { - total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; - buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; - pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); - } - break; - } - - for (; i < h; ++i) { - STBTT_assert(pixels[i*stride_in_bytes] == 0); - total -= buffer[i & STBTT__OVER_MASK]; - pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); - } - - pixels += 1; - } + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j = 0; j < w; ++j) + { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) + { + case 2: + for (i = 0; i <= safe_h; ++i) + { + total += pixels[i * stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i * stride_in_bytes]; + pixels[i * stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i = 0; i <= safe_h; ++i) + { + total += pixels[i * stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i * stride_in_bytes]; + pixels[i * stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i = 0; i <= safe_h; ++i) + { + total += pixels[i * stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i * stride_in_bytes]; + pixels[i * stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i = 0; i <= safe_h; ++i) + { + total += pixels[i * stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i * stride_in_bytes]; + pixels[i * stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i = 0; i <= safe_h; ++i) + { + total += pixels[i * stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i + kernel_width) & STBTT__OVER_MASK] = pixels[i * stride_in_bytes]; + pixels[i * stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) + { + STBTT_assert(pixels[i * stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i * stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } } static float stbtt__oversample_shift(int oversample) { - if (!oversample) - return 0.0f; + if (!oversample) + return 0.0f; - // The prefilter is a box filter of width "oversample", - // which shifts phase by (oversample - 1)/2 pixels in - // oversampled space. We want to shift in the opposite - // direction to counter this. - return (float)-(oversample - 1) / (2.0f * (float)oversample); + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float) -(oversample - 1) / (2.0f * (float) oversample); } // rects array must be big enough to accommodate all characters in the given ranges STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) { - int i,j,k; - - k=0; - for (i=0; i < num_ranges; ++i) { - float fh = ranges[i].font_size; - float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); - ranges[i].h_oversample = (unsigned char) spc->h_oversample; - ranges[i].v_oversample = (unsigned char) spc->v_oversample; - for (j=0; j < ranges[i].num_chars; ++j) { - int x0,y0,x1,y1; - int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; - int glyph = stbtt_FindGlyphIndex(info, codepoint); - stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, - scale * spc->h_oversample, - scale * spc->v_oversample, - 0,0, - &x0,&y0,&x1,&y1); - rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); - rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); - ++k; - } - } - - return k; + int i, j, k; + + k = 0; + for (i = 0; i < num_ranges; ++i) + { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j = 0; j < ranges[i].num_chars; ++j) + { + int x0, y0, x1, y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0, 0, + &x0, &y0, &x1, &y1); + rects[k].w = (stbrp_coord) (x1 - x0 + spc->padding + spc->h_oversample - 1); + rects[k].h = (stbrp_coord) (y1 - y0 + spc->padding + spc->v_oversample - 1); + ++k; + } + } + + return k; } // rects array must be big enough to accommodate all characters in the given ranges STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) { - int i,j,k, return_value = 1; - - // save current values - int old_h_over = spc->h_oversample; - int old_v_over = spc->v_oversample; - - k = 0; - for (i=0; i < num_ranges; ++i) { - float fh = ranges[i].font_size; - float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); - float recip_h,recip_v,sub_x,sub_y; - spc->h_oversample = ranges[i].h_oversample; - spc->v_oversample = ranges[i].v_oversample; - recip_h = 1.0f / spc->h_oversample; - recip_v = 1.0f / spc->v_oversample; - sub_x = stbtt__oversample_shift(spc->h_oversample); - sub_y = stbtt__oversample_shift(spc->v_oversample); - for (j=0; j < ranges[i].num_chars; ++j) { - stbrp_rect *r = &rects[k]; - if (r->was_packed) { - stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; - int advance, lsb, x0,y0,x1,y1; - int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; - int glyph = stbtt_FindGlyphIndex(info, codepoint); - stbrp_coord pad = (stbrp_coord) spc->padding; - - // pad on left and top - r->x += pad; - r->y += pad; - r->w -= pad; - r->h -= pad; - stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); - stbtt_GetGlyphBitmapBox(info, glyph, - scale * spc->h_oversample, - scale * spc->v_oversample, - &x0,&y0,&x1,&y1); - stbtt_MakeGlyphBitmapSubpixel(info, - spc->pixels + r->x + r->y*spc->stride_in_bytes, - r->w - spc->h_oversample+1, - r->h - spc->v_oversample+1, - spc->stride_in_bytes, - scale * spc->h_oversample, - scale * spc->v_oversample, - 0,0, - glyph); - - if (spc->h_oversample > 1) - stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, - r->w, r->h, spc->stride_in_bytes, - spc->h_oversample); - - if (spc->v_oversample > 1) - stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, - r->w, r->h, spc->stride_in_bytes, - spc->v_oversample); - - bc->x0 = (stbtt_int16) r->x; - bc->y0 = (stbtt_int16) r->y; - bc->x1 = (stbtt_int16) (r->x + r->w); - bc->y1 = (stbtt_int16) (r->y + r->h); - bc->xadvance = scale * advance; - bc->xoff = (float) x0 * recip_h + sub_x; - bc->yoff = (float) y0 * recip_v + sub_y; - bc->xoff2 = (x0 + r->w) * recip_h + sub_x; - bc->yoff2 = (y0 + r->h) * recip_v + sub_y; - } else { - return_value = 0; // if any fail, report failure - } - - ++k; - } - } - - // restore original values - spc->h_oversample = old_h_over; - spc->v_oversample = old_v_over; - - return return_value; + int i, j, k, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i = 0; i < num_ranges; ++i) + { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h, recip_v, sub_x, sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j = 0; j < ranges[i].num_chars; ++j) + { + stbrp_rect *r = &rects[k]; + if (r->was_packed) + { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0, y0, x1, y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0, &y0, &x1, &y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y * spc->stride_in_bytes, + r->w - spc->h_oversample + 1, + r->h - spc->v_oversample + 1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0, 0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y * spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y * spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + } + else + { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; } STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) { - stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); } STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) { - stbtt_fontinfo info; - int i,j,n, return_value = 1; - //stbrp_context *context = (stbrp_context *) spc->pack_info; - stbrp_rect *rects; + stbtt_fontinfo info; + int i, j, n, return_value = 1; + // stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i = 0; i < num_ranges; ++i) + for (j = 0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; - // flag all characters as NOT packed - for (i=0; i < num_ranges; ++i) - for (j=0; j < ranges[i].num_chars; ++j) - ranges[i].chardata_for_range[j].x0 = - ranges[i].chardata_for_range[j].y0 = - ranges[i].chardata_for_range[j].x1 = - ranges[i].chardata_for_range[j].y1 = 0; + n = 0; + for (i = 0; i < num_ranges; ++i) + n += ranges[i].num_chars; - n = 0; - for (i=0; i < num_ranges; ++i) - n += ranges[i].num_chars; - - rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); - if (rects == NULL) - return 0; + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; - info.userdata = spc->user_allocator_context; - stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata, font_index)); - n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); - stbtt_PackFontRangesPackRects(spc, rects, n); - - return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + stbtt_PackFontRangesPackRects(spc, rects, n); - STBTT_free(rects, spc->user_allocator_context); - return return_value; + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; } STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, unsigned char *fontdata, int font_index, float font_size, - int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) { - stbtt_pack_range range; - range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; - range.array_of_unicode_codepoints = NULL; - range.num_chars = num_chars_in_range; - range.chardata_for_range = chardata_for_range; - range.font_size = font_size; - return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); } STBTT_DEF void stbtt_GetPackedQuad(stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) { - float ipw = 1.0f / pw, iph = 1.0f / ph; - stbtt_packedchar *b = chardata + char_index; - - if (align_to_integer) { - float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); - float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); - q->x0 = x; - q->y0 = y; - q->x1 = x + b->xoff2 - b->xoff; - q->y1 = y + b->yoff2 - b->yoff; - } else { - q->x0 = *xpos + b->xoff; - q->y0 = *ypos + b->yoff; - q->x1 = *xpos + b->xoff2; - q->y1 = *ypos + b->yoff2; - } - - q->s0 = b->x0 * ipw; - q->t0 = b->y0 * iph; - q->s1 = b->x1 * ipw; - q->t1 = b->y1 * iph; - - *xpos += b->xadvance; + float ipw = 1.0f / pw, iph = 1.0f / ph; + stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) + { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } + else + { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; } - ////////////////////////////////////////////////////////////////////////////// // // font name matching -- recommended not to use this // // check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string -static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(const stbtt_uint8 *s1, stbtt_int32 len1, const stbtt_uint8 *s2, stbtt_int32 len2) -{ - stbtt_int32 i=0; - - // convert utf16 to utf8 and compare the results while converting - while (len2) { - stbtt_uint16 ch = s2[0]*256 + s2[1]; - if (ch < 0x80) { - if (i >= len1) return -1; - if (s1[i++] != ch) return -1; - } else if (ch < 0x800) { - if (i+1 >= len1) return -1; - if (s1[i++] != 0xc0 + (ch >> 6)) return -1; - if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; - } else if (ch >= 0xd800 && ch < 0xdc00) { - stbtt_uint32 c; - stbtt_uint16 ch2 = s2[2]*256 + s2[3]; - if (i+3 >= len1) return -1; - c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; - if (s1[i++] != 0xf0 + (c >> 18)) return -1; - if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; - if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; - if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; - s2 += 2; // plus another 2 below - len2 -= 2; - } else if (ch >= 0xdc00 && ch < 0xe000) { - return -1; - } else { - if (i+2 >= len1) return -1; - if (s1[i++] != 0xe0 + (ch >> 12)) return -1; - if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; - if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; - } - s2 += 2; - len2 -= 2; - } - return i; -} - -STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) -{ - return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((const stbtt_uint8*) s1, len1, (const stbtt_uint8*) s2, len2); +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(const stbtt_uint8 *s1, stbtt_int32 len1, const stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i = 0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) + { + stbtt_uint16 ch = s2[0] * 256 + s2[1]; + if (ch < 0x80) + { + if (i >= len1) + return -1; + if (s1[i++] != ch) + return -1; + } + else if (ch < 0x800) + { + if (i + 1 >= len1) + return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) + return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) + return -1; + } + else if (ch >= 0xd800 && ch < 0xdc00) + { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2] * 256 + s2[3]; + if (i + 3 >= len1) + return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) + return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) + return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) + return -1; + if (s1[i++] != 0x80 + ((c) & 0x3f)) + return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } + else if (ch >= 0xdc00 && ch < 0xe000) + { + return -1; + } + else + { + if (i + 2 >= len1) + return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) + return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) + return -1; + if (s1[i++] != 0x80 + ((ch) & 0x3f)) + return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((const stbtt_uint8 *) s1, len1, (const stbtt_uint8 *) s2, len2); } // returns results in whatever encoding you request... but note that 2-byte encodings // will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) { - stbtt_int32 i,count,stringOffset; - stbtt_uint8 *fc = font->data; - stbtt_uint32 offset = font->fontstart; - stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); - if (!nm) return NULL; - - count = ttUSHORT(fc+nm+2); - stringOffset = nm + ttUSHORT(fc+nm+4); - for (i=0; i < count; ++i) { - stbtt_uint32 loc = nm + 6 + 12 * i; - if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) - && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { - *length = ttUSHORT(fc+loc+8); - return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); - } - } - return NULL; + stbtt_int32 i, count, stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) + return NULL; + + count = ttUSHORT(fc + nm + 2); + stringOffset = nm + ttUSHORT(fc + nm + 4); + for (i = 0; i < count; ++i) + { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc + loc + 0) && encodingID == ttUSHORT(fc + loc + 2) && languageID == ttUSHORT(fc + loc + 4) && nameID == ttUSHORT(fc + loc + 6)) + { + *length = ttUSHORT(fc + loc + 8); + return (const char *) (fc + stringOffset + ttUSHORT(fc + loc + 10)); + } + } + return NULL; } static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) { - stbtt_int32 i; - stbtt_int32 count = ttUSHORT(fc+nm+2); - stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); - - for (i=0; i < count; ++i) { - stbtt_uint32 loc = nm + 6 + 12 * i; - stbtt_int32 id = ttUSHORT(fc+loc+6); - if (id == target_id) { - // find the encoding - stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); - - // is this a Unicode encoding? - if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { - stbtt_int32 slen = ttUSHORT(fc+loc+8); - stbtt_int32 off = ttUSHORT(fc+loc+10); - - // check if there's a prefix match - stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); - if (matchlen >= 0) { - // check for target_id+1 immediately following, with same encoding & language - if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { - slen = ttUSHORT(fc+loc+12+8); - off = ttUSHORT(fc+loc+12+10); - if (slen == 0) { - if (matchlen == nlen) - return 1; - } else if (matchlen < nlen && name[matchlen] == ' ') { - ++matchlen; - if (stbtt_CompareUTF8toUTF16_bigendian((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) - return 1; - } - } else { - // if nothing immediately following - if (matchlen == nlen) - return 1; - } - } - } - - // @TODO handle other encodings - } - } - return 0; + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc + nm + 2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc + nm + 4); + + for (i = 0; i < count; ++i) + { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc + loc + 6); + if (id == target_id) + { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc + loc + 0), encoding = ttUSHORT(fc + loc + 2), language = ttUSHORT(fc + loc + 4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) + { + stbtt_int32 slen = ttUSHORT(fc + loc + 8); + stbtt_int32 off = ttUSHORT(fc + loc + 10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc + stringOffset + off, slen); + if (matchlen >= 0) + { + // check for target_id+1 immediately following, with same encoding & language + if (i + 1 < count && ttUSHORT(fc + loc + 12 + 6) == next_id && ttUSHORT(fc + loc + 12) == platform && ttUSHORT(fc + loc + 12 + 2) == encoding && ttUSHORT(fc + loc + 12 + 4) == language) + { + slen = ttUSHORT(fc + loc + 12 + 8); + off = ttUSHORT(fc + loc + 12 + 10); + if (slen == 0) + { + if (matchlen == nlen) + return 1; + } + else if (matchlen < nlen && name[matchlen] == ' ') + { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian((char *) (name + matchlen), nlen - matchlen, (char *) (fc + stringOffset + off), slen)) + return 1; + } + } + else + { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; } static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) { - stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); - stbtt_uint32 nm,hd; - if (!stbtt__isfont(fc+offset)) return 0; - - // check italics/bold/underline flags in macStyle... - if (flags) { - hd = stbtt__find_table(fc, offset, "head"); - if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; - } - - nm = stbtt__find_table(fc, offset, "name"); - if (!nm) return 0; - - if (flags) { - // if we checked the macStyle flags, then just check the family and ignore the subfamily - if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; - if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; - if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; - } else { - if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; - if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; - if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; - } - - return 0; + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm, hd; + if (!stbtt__isfont(fc + offset)) + return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) + { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc + hd + 44) & 7) != (flags & 7)) + return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) + return 0; + + if (flags) + { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) + return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) + return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) + return 1; + } + else + { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) + return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) + return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) + return 1; + } + + return 0; } STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *font_collection, const char *name_utf8, stbtt_int32 flags) { - stbtt_int32 i; - for (i=0;;++i) { - stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); - if (off < 0) return off; - if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) - return off; - } + stbtt_int32 i; + for (i = 0;; ++i) + { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) + return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8 *) name_utf8, flags)) + return off; + } } -#endif // STB_TRUETYPE_IMPLEMENTATION - +#endif // STB_TRUETYPE_IMPLEMENTATION // FULL VERSION HISTORY // diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index d788d372..6fc4012d 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -1,6 +1,22 @@ +/* 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 "renderer.h" #include "audio_system.h" +#include "renderer.h" // Include ImGui headers #include "imgui/imgui.h" @@ -10,1059 +26,1137 @@ // 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() +{ + // Constructor implementation } -ImGuiSystem::~ImGuiSystem() { - // Destructor implementation - Cleanup(); +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); - - // 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; +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); + + // 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::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::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; - } - - ImGui::NewFrame(); - - // Loading overlay: show only a fullscreen progress bar while the model - // itself is loading. Once the scene is ready and geometry is visible, - // we no longer block the view with a full-screen progress bar. - if (renderer) { - const uint32_t scheduled = renderer->GetTextureTasksScheduled(); - const uint32_t completed = renderer->GetTextureTasksCompleted(); - const bool modelLoading = renderer->IsLoading(); - if (modelLoading) { - ImGuiIO& io = ImGui::GetIO(); - // Suppress right-click while loading - if (io.MouseDown[1]) io.MouseDown[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(); - float frac = (scheduled > 0) ? (float)completed / (float)scheduled : 0.0f; - ImGui::ProgressBar(frac, ImVec2(barWidth, 0.0f)); - ImGui::Dummy(ImVec2(0.0f, 10.0f)); - ImGui::SetCursorPosX(barX); - ImGui::Text("Loading scene..."); - ImGui::EndGroup(); - ImGui::PopStyleVar(); - } - ImGui::End(); - // Do not draw the rest of the UI until loading finishes - 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(); - - 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 = 60.0f; - const ImVec2 winPos(dispSize.x - windowWidth - 10.0f, 10.0f); - - 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)); - } - ImGui::End(); - } - } - - // Create HRTF Audio Control UI - ImGui::Begin("HRTF Audio Controls"); - ImGui::Text("Hello, Vulkan!"); - // Lighting Controls - BRDF/PBR is now the default lighting model - ImGui::Separator(); - ImGui::Text("Lighting Controls:"); - - // Invert the checkbox logic - now controls basic lighting instead of PBR - bool useBasicLighting = !pbrEnabled; - if (ImGui::Checkbox("Use Basic Lighting (Phong)", &useBasicLighting)) { - pbrEnabled = !useBasicLighting; - std::cout << "Lighting mode: " << (pbrEnabled ? "BRDF/PBR (default)" : "Basic Phong") << std::endl; - } - - if (pbrEnabled) { - 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"); - } - - if (pbrEnabled) { - // BRDF Quality Controls - ImGui::Separator(); - ImGui::Text("BRDF Quality Controls:"); - - // Gamma correction slider - static float gamma = 2.2f; - if (ImGui::SliderFloat("Gamma Correction", &gamma, 1.0f, 3.0f, "%.2f")) { - // Update gamma in renderer - if (renderer) { - renderer->SetGamma(gamma); - } - std::cout << "Gamma set to: " << gamma << std::endl; - } - ImGui::SameLine(); - if (ImGui::Button("Reset##Gamma")) { - gamma = 2.2f; - if (renderer) { - renderer->SetGamma(gamma); - } - std::cout << "Gamma reset to: " << gamma << std::endl; - } - - // Exposure slider - static float exposure = 1.2f; // Default tuned to avoid washout - if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 10.0f, "%.2f")) { - // Update exposure in renderer - if (renderer) { - renderer->SetExposure(exposure); - } - std::cout << "Exposure set to: " << exposure << std::endl; - } - ImGui::SameLine(); - if (ImGui::Button("Reset##Exposure")) { - exposure = 1.2f; // Reset to a sane default to avoid washout - if (renderer) { - renderer->SetExposure(exposure); - } - std::cout << "Exposure reset to: " << exposure << std::endl; - } - - ImGui::Text("Tip: Adjust gamma if scene looks too dark/bright"); - ImGui::Text("Tip: Adjust exposure if scene looks washed out"); - } else { - ImGui::Text("Note: Quality controls affect BRDF rendering only"); - } - - ImGui::Separator(); - 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::NewFrame() +{ + if (!initialized) + { + return; + } + + // Reset the flag at the start of each frame + frameAlreadyRendered = false; + + ImGui::NewFrame(); + + // Loading overlay: show only a fullscreen progress bar while the model + // itself is loading. Once the scene is ready and geometry is visible, + // we no longer block the view with a full-screen progress bar. + if (renderer) + { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + const bool modelLoading = renderer->IsLoading(); + if (modelLoading) + { + ImGuiIO &io = ImGui::GetIO(); + // Suppress right-click while loading + if (io.MouseDown[1]) + io.MouseDown[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(); + float frac = (scheduled > 0) ? (float) completed / (float) scheduled : 0.0f; + ImGui::ProgressBar(frac, ImVec2(barWidth, 0.0f)); + ImGui::Dummy(ImVec2(0.0f, 10.0f)); + ImGui::SetCursorPosX(barX); + ImGui::Text("Loading scene..."); + ImGui::EndGroup(); + ImGui::PopStyleVar(); + } + ImGui::End(); + ImGui::Render(); + frameAlreadyRendered = true; + 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(); + + 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; + const ImVec2 winPos(dispSize.x - windowWidth - 10.0f, 10.0f); + + 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; - } - - 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 - std::array vertexBuffersArr = {*vertexBuffers[frameIndex]}; - std::array offsets = {}; - commandBuffer.bindVertexBuffers(0, vertexBuffersArr, offsets); - commandBuffer.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); - - // Render command lists - int vertexOffset = 0; - int indexOffset = 0; - - for (int i = 0; i < drawData->CmdListsCount; 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 (font texture) - 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::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; + } + + 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 + std::array vertexBuffersArr = {*vertexBuffers[frameIndex]}; + std::array offsets = {}; + commandBuffer.bindVertexBuffers(0, vertexBuffersArr, offsets); + commandBuffer.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); + + // Render command lists + int vertexOffset = 0; + int indexOffset = 0; + + for (int i = 0; i < drawData->CmdListsCount; 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 (font texture) + 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; - } +void ImGuiSystem::HandleMouse(float x, float y, uint32_t buttons) +{ + if (!initialized) + { + return; + } - ImGuiIO& io = ImGui::GetIO(); + ImGuiIO &io = ImGui::GetIO(); - // Update mouse position - io.MousePos = ImVec2(x, y); + // Update mouse position + io.MousePos = ImVec2(x, y); - // Update mouse buttons - io.MouseDown[0] = (buttons & 0x01) != 0; // Left button - io.MouseDown[1] = (buttons & 0x02) != 0; // Right button - io.MouseDown[2] = (buttons & 0x04) != 0; // Middle button + // Update mouse buttons + io.MouseDown[0] = (buttons & 0x01) != 0; // Left button + io.MouseDown[1] = (buttons & 0x02) != 0; // Right button + io.MouseDown[2] = (buttons & 0x04) != 0; // Middle button } -void ImGuiSystem::HandleKeyboard(uint32_t key, bool pressed) { - if (!initialized) { - return; - } - - ImGuiIO& io = ImGui::GetIO(); - - // Update key state - if (key < 512) { - io.KeysDown[key] = pressed; - } - - // Update modifier keys - // Using GLFW key codes instead of Windows-specific VK_* constants - io.KeyCtrl = io.KeysDown[341] || io.KeysDown[345]; // Left/Right Control - io.KeyShift = io.KeysDown[340] || io.KeysDown[344]; // Left/Right Shift - io.KeyAlt = io.KeysDown[342] || io.KeysDown[346]; // Left/Right Alt - io.KeySuper = io.KeysDown[343] || io.KeysDown[347]; // Left/Right Super +void ImGuiSystem::HandleKeyboard(uint32_t key, bool pressed) +{ + if (!initialized) + { + return; + } + + ImGuiIO &io = ImGui::GetIO(); + + // Update key state + if (key < 512) + { + io.KeysDown[key] = pressed; + } + + // Update modifier keys + // Using GLFW key codes instead of Windows-specific VK_* constants + io.KeyCtrl = io.KeysDown[341] || io.KeysDown[345]; // Left/Right Control + io.KeyShift = io.KeysDown[340] || io.KeysDown[344]; // Left/Right Shift + io.KeyAlt = io.KeysDown[342] || io.KeysDown[346]; // Left/Right Alt + io.KeySuper = io.KeysDown[343] || io.KeysDown[347]; // Left/Right Super } -void ImGuiSystem::HandleChar(uint32_t c) { - if (!initialized) { - return; - } +void ImGuiSystem::HandleChar(uint32_t c) +{ + if (!initialized) + { + return; + } - ImGuiIO& io = ImGui::GetIO(); - io.AddInputCharacter(c); + ImGuiIO &io = ImGui::GetIO(); + io.AddInputCharacter(c); } -void ImGuiSystem::HandleResize(uint32_t width, uint32_t height) { - if (!initialized) { - return; - } +void ImGuiSystem::HandleResize(uint32_t width, uint32_t height) +{ + if (!initialized) + { + return; + } - this->width = width; - this->height = height; + this->width = width; + this->height = height; - ImGuiIO& io = ImGui::GetIO(); - io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + ImGuiIO &io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); } -bool ImGuiSystem::WantCaptureKeyboard() const { - if (!initialized) { - return false; - } +bool ImGuiSystem::WantCaptureKeyboard() const +{ + if (!initialized) + { + return false; + } - return ImGui::GetIO().WantCaptureKeyboard; + return ImGui::GetIO().WantCaptureKeyboard; } -bool ImGuiSystem::WantCaptureMouse() const { - if (!initialized) { - return false; - } +bool ImGuiSystem::WantCaptureMouse() const +{ + if (!initialized) + { + return false; + } - return ImGui::GetIO().WantCaptureMouse; + return ImGui::GetIO().WantCaptureMouse; } -bool ImGuiSystem::createResources() { - // Create all Vulkan resources needed for ImGui rendering - if (!createFontTexture()) { - return false; - } - - if (!createDescriptorSetLayout()) { - return false; - } - - if (!createDescriptorPool()) { - return false; - } - - if (!createDescriptorSet()) { - return false; - } - - if (!createPipelineLayout()) { - return false; - } - - if (!createPipeline()) { - return false; - } - - return true; +bool ImGuiSystem::createResources() +{ + // Create all Vulkan resources needed for ImGui rendering + if (!createFontTexture()) + { + return false; + } + + if (!createDescriptorSetLayout()) + { + return false; + } + + if (!createDescriptorPool()) + { + return false; + } + + if (!createDescriptorSet()) + { + return false; + } + + if (!createPipelineLayout()) + { + return false; + } + + if (!createPipeline()) + { + return false; + } + + return true; } -bool ImGuiSystem::createFontTexture() { - // Get font texture from ImGui - ImGuiIO& io = ImGui::GetIO(); - unsigned char* fontData; - int texWidth, texHeight; - io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); - vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); - - try { - // Create the font image - vk::ImageCreateInfo imageInfo; - imageInfo.imageType = vk::ImageType::e2D; - imageInfo.format = vk::Format::eR8G8B8A8Unorm; - 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; - - const vk::raii::Device& device = renderer->GetRaiiDevice(); - 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 a staging buffer for uploading the font 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 font data to staging buffer - void* data = stagingBufferMemory.mapMemory(0, uploadSize); - memcpy(data, fontData, uploadSize); - stagingBufferMemory.unmapMemory(); - - // Transition image layout and copy data - renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, - vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); - renderer->CopyBufferToImage(*stagingBuffer, *fontImage, - static_cast(texWidth), static_cast(texHeight)); - renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, - vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); - - // Staging buffer and memory will be automatically cleaned up by RAII - - // Create image view - vk::ImageViewCreateInfo viewInfo; - viewInfo.image = *fontImage; - viewInfo.viewType = vk::ImageViewType::e2D; - viewInfo.format = vk::Format::eR8G8B8A8Unorm; - 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); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create font texture: " << e.what() << std::endl; - return false; - } +bool ImGuiSystem::createFontTexture() +{ + // Get font texture from ImGui + ImGuiIO &io = ImGui::GetIO(); + unsigned char *fontData; + int texWidth, texHeight; + io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); + vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); + + try + { + // Create the font image + vk::ImageCreateInfo imageInfo; + imageInfo.imageType = vk::ImageType::e2D; + imageInfo.format = vk::Format::eR8G8B8A8Unorm; + 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; + + const vk::raii::Device &device = renderer->GetRaiiDevice(); + 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 a staging buffer for uploading the font 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 font data to staging buffer + void *data = stagingBufferMemory.mapMemory(0, uploadSize); + memcpy(data, fontData, uploadSize); + stagingBufferMemory.unmapMemory(); + + // Transition image layout and copy data + renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + renderer->CopyBufferToImage(*stagingBuffer, *fontImage, + static_cast(texWidth), static_cast(texHeight)); + renderer->TransitionImageLayout(*fontImage, vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Staging buffer and memory will be automatically cleaned up by RAII + + // Create image view + vk::ImageViewCreateInfo viewInfo; + viewInfo.image = *fontImage; + viewInfo.viewType = vk::ImageViewType::e2D; + viewInfo.format = vk::Format::eR8G8B8A8Unorm; + 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); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create font texture: " << e.what() << std::endl; + return false; + } } -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::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::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; - - // Update descriptor set - vk::DescriptorImageInfo imageInfo; - imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; - imageInfo.imageView = *fontView; - imageInfo.sampler = *fontSampler; - - vk::WriteDescriptorSet writeSet; - writeSet.dstSet = *descriptorSet; - writeSet.descriptorCount = 1; - writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; - writeSet.pImageInfo = &imageInfo; - writeSet.dstBinding = 0; - - device.updateDescriptorSets({writeSet}, {}); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create descriptor set: " << 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; + + // Update descriptor set + vk::DescriptorImageInfo imageInfo; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageInfo.imageView = *fontView; + imageInfo.sampler = *fontSampler; + + vk::WriteDescriptorSet writeSet; + writeSet.dstSet = *descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageInfo; + writeSet.dstBinding = 0; + + device.updateDescriptorSets({writeSet}, {}); + + 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::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; - } +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 (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 (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 - 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; - } +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 (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 (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 + 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/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h index d93e026f..1ffdc5ec 100644 --- a/attachments/simple_engine/imgui_system.h +++ b/attachments/simple_engine/imgui_system.h @@ -1,11 +1,27 @@ +/* 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 // Forward declarations class Renderer; @@ -19,212 +35,240 @@ struct ImGuiContext; * This class implements the ImGui integration as described in the GUI chapter: * @see en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc */ -class ImGuiSystem { -public: - /** - * @brief Default constructor. - */ - ImGuiSystem(); - - // Constructor-based initialization to replace separate Initialize() calls - ImGuiSystem(Renderer* renderer, uint32_t width, uint32_t height) { - if (!Initialize(renderer, width, height)) { - throw std::runtime_error("ImGuiSystem: initialization failed"); - } - } - - /** - * @brief Destructor for proper cleanup. - */ - ~ImGuiSystem(); - - /** - * @brief Initialize the ImGui system. - * @param renderer Pointer to the renderer. - * @param width The width of the window. - * @param height The height of the window. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(Renderer* renderer, uint32_t width, uint32_t height); - - /** - * @brief Clean up ImGui resources. - */ - void Cleanup(); - - /** - * @brief Start a new ImGui frame. - */ - void NewFrame(); - - /** - * @brief Render the ImGui frame. - * @param commandBuffer The command buffer to record rendering commands to. - */ - void Render(vk::raii::CommandBuffer & commandBuffer, uint32_t frameIndex); - - /** - * @brief Handle mouse input. - * @param x The x-coordinate of the mouse. - * @param y The y-coordinate of the mouse. - * @param buttons The state of the mouse buttons. - */ - void HandleMouse(float x, float y, uint32_t buttons); - - /** - * @brief Handle keyboard input. - * @param key The key code. - * @param pressed Whether the key was pressed or released. - */ - void HandleKeyboard(uint32_t key, bool pressed); - - /** - * @brief Handle character input. - * @param c The character. - */ - void HandleChar(uint32_t c); - - /** - * @brief Handle window resize. - * @param width The new width of the window. - * @param height The new height of the window. - */ - void HandleResize(uint32_t width, uint32_t height); - - /** - * @brief Check if ImGui wants to capture keyboard input. - * @return True if ImGui wants to capture keyboard input, false otherwise. - */ - bool WantCaptureKeyboard() const; - - /** - * @brief Check if ImGui wants to capture mouse input. - * @return True if ImGui wants to capture mouse input, false otherwise. - */ - bool WantCaptureMouse() const; - - /** - * @brief Set the audio system reference for audio controls. - * @param audioSystem Pointer to the audio system. - */ - void SetAudioSystem(AudioSystem* audioSystem); - - /** - * @brief Get the current PBR rendering state. - * @return True if PBR rendering is enabled, false otherwise. - */ - bool IsPBREnabled() const { return pbrEnabled; } - - /** - * @brief Get the current ball-only rendering state. - * @return True if ball-only rendering is enabled, false otherwise. - */ - bool IsBallOnlyRenderingEnabled() const { return ballOnlyRenderingEnabled; } - - /** - * @brief Get the current camera tracking state. - * @return True if camera tracking is enabled, false otherwise. - */ - bool IsCameraTrackingEnabled() const { return cameraTrackingEnabled; } - -private: - // ImGui context - ImGuiContext* context = nullptr; - - // Renderer reference - Renderer* renderer = nullptr; - - // Audio system reference - AudioSystem* audioSystem = nullptr; - AudioSource* audioSource = nullptr; - AudioSource* debugPingSource = nullptr; - - // Audio position tracking - float audioSourceX = 1.0f; - float audioSourceY = 0.0f; - float audioSourceZ = 0.0f; - - // Vulkan resources - vk::raii::DescriptorPool descriptorPool = nullptr; - vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; - vk::raii::DescriptorSet descriptorSet = nullptr; - vk::raii::PipelineLayout pipelineLayout = nullptr; - vk::raii::Pipeline pipeline = nullptr; - vk::raii::Sampler fontSampler = nullptr; - vk::raii::Image fontImage = nullptr; - vk::raii::DeviceMemory fontMemory = nullptr; - vk::raii::ImageView fontView = nullptr; - // Per-frame dynamic buffers to avoid GPU/CPU contention when frames are in flight - std::vector vertexBuffers; - std::vector vertexBufferMemories; - std::vector indexBuffers; - std::vector indexBufferMemories; - std::vector vertexCounts; - std::vector indexCounts; - - // Window dimensions - uint32_t width = 0; - uint32_t height = 0; - - // Mouse state - float mouseX = 0.0f; - float mouseY = 0.0f; - uint32_t mouseButtons = 0; - - // Initialization flag - bool initialized = false; - - // PBR rendering state - bool pbrEnabled = true; - - // Ball-only rendering and camera tracking state - bool ballOnlyRenderingEnabled = false; - bool cameraTrackingEnabled = false; - - /** - * @brief Create Vulkan resources for ImGui. - * @return True if creation was successful, false otherwise. - */ - bool createResources(); - - /** - * @brief Create font texture. - * @return True if creation was successful, false otherwise. - */ - bool createFontTexture(); - - /** - * @brief Create descriptor set layout. - * @return True if creation was successful, false otherwise. - */ - bool createDescriptorSetLayout(); - - /** - * @brief Create descriptor pool. - * @return True if creation was successful, false otherwise. - */ - bool createDescriptorPool(); - - /** - * @brief Create descriptor set. - * @return True if creation was successful, false otherwise. - */ - bool createDescriptorSet(); - - /** - * @brief Create pipeline layout. - * @return True if creation was successful, false otherwise. - */ - bool createPipelineLayout(); - - /** - * @brief Create pipeline. - * @return True if creation was successful, false otherwise. - */ - bool createPipeline(); - - /** - * @brief Update vertex and index buffers. - */ - void updateBuffers(uint32_t frameIndex); +class ImGuiSystem +{ + public: + /** + * @brief Default constructor. + */ + ImGuiSystem(); + + // Constructor-based initialization to replace separate Initialize() calls + ImGuiSystem(Renderer *renderer, uint32_t width, uint32_t height) + { + if (!Initialize(renderer, width, height)) + { + throw std::runtime_error("ImGuiSystem: initialization failed"); + } + } + + /** + * @brief Destructor for proper cleanup. + */ + ~ImGuiSystem(); + + /** + * @brief Initialize the ImGui system. + * @param renderer Pointer to the renderer. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Renderer *renderer, uint32_t width, uint32_t height); + + /** + * @brief Clean up ImGui resources. + */ + void Cleanup(); + + /** + * @brief Start a new ImGui frame. + */ + void NewFrame(); + + /** + * @brief Render the ImGui frame. + * @param commandBuffer The command buffer to record rendering commands to. + */ + void Render(vk::raii::CommandBuffer &commandBuffer, uint32_t frameIndex); + + /** + * @brief Handle mouse input. + * @param x The x-coordinate of the mouse. + * @param y The y-coordinate of the mouse. + * @param buttons The state of the mouse buttons. + */ + void HandleMouse(float x, float y, uint32_t buttons); + + /** + * @brief Handle keyboard input. + * @param key The key code. + * @param pressed Whether the key was pressed or released. + */ + void HandleKeyboard(uint32_t key, bool pressed); + + /** + * @brief Handle character input. + * @param c The character. + */ + void HandleChar(uint32_t c); + + /** + * @brief Handle window resize. + * @param width The new width of the window. + * @param height The new height of the window. + */ + void HandleResize(uint32_t width, uint32_t height); + + /** + * @brief Check if ImGui wants to capture keyboard input. + * @return True if ImGui wants to capture keyboard input, false otherwise. + */ + bool WantCaptureKeyboard() const; + + /** + * @brief Check if ImGui wants to capture mouse input. + * @return True if ImGui wants to capture mouse input, false otherwise. + */ + bool WantCaptureMouse() const; + + /** + * @brief Check if ImGui has already been rendered for the current frame. + * @return True if Render() was already called in NewFrame(), false otherwise. + */ + bool IsFrameRendered() const + { + return frameAlreadyRendered; + } + + /** + * @brief Set the audio system reference for audio controls. + * @param audioSystem Pointer to the audio system. + */ + void SetAudioSystem(AudioSystem *audioSystem); + + /** + * @brief Get the current PBR rendering state. + * @return True if PBR rendering is enabled, false otherwise. + */ + bool IsPBREnabled() const + { + return pbrEnabled; + } + + /** + * @brief Get the current ball-only rendering state. + * @return True if ball-only rendering is enabled, false otherwise. + */ + bool IsBallOnlyRenderingEnabled() const + { + return ballOnlyRenderingEnabled; + } + + /** + * @brief Get the current camera tracking state. + * @return True if camera tracking is enabled, false otherwise. + */ + bool IsCameraTrackingEnabled() const + { + return cameraTrackingEnabled; + } + void SetPBREnabled(bool pbr) + { + pbrEnabled = pbr; + }; + + private: + // ImGui context + ImGuiContext *context = nullptr; + + // Renderer reference + Renderer *renderer = nullptr; + + // Audio system reference + AudioSystem *audioSystem = nullptr; + AudioSource *audioSource = nullptr; + AudioSource *debugPingSource = nullptr; + + // Audio position tracking + float audioSourceX = 1.0f; + float audioSourceY = 0.0f; + float audioSourceZ = 0.0f; + + // Vulkan resources + vk::raii::DescriptorPool descriptorPool = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorSet descriptorSet = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline pipeline = nullptr; + vk::raii::Sampler fontSampler = nullptr; + vk::raii::Image fontImage = nullptr; + vk::raii::DeviceMemory fontMemory = nullptr; + vk::raii::ImageView fontView = nullptr; + // Per-frame dynamic buffers to avoid GPU/CPU contention when frames are in flight + std::vector vertexBuffers; + std::vector vertexBufferMemories; + std::vector indexBuffers; + std::vector indexBufferMemories; + std::vector vertexCounts; + std::vector indexCounts; + + // Window dimensions + uint32_t width = 0; + uint32_t height = 0; + + // Mouse state + float mouseX = 0.0f; + float mouseY = 0.0f; + uint32_t mouseButtons = 0; + + // Initialization flag + bool initialized = false; + + // PBR rendering state + bool pbrEnabled = true; + + // Ball-only rendering and camera tracking state + bool ballOnlyRenderingEnabled = false; + bool cameraTrackingEnabled = false; + + // Track if ImGui::Render() was already called in NewFrame() (during loading overlay) + bool frameAlreadyRendered = false; + + /** + * @brief Create Vulkan resources for ImGui. + * @return True if creation was successful, false otherwise. + */ + bool createResources(); + + /** + * @brief Create font texture. + * @return True if creation was successful, false otherwise. + */ + bool createFontTexture(); + + /** + * @brief Create descriptor set layout. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorSetLayout(); + + /** + * @brief Create descriptor pool. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorPool(); + + /** + * @brief Create descriptor set. + * @return True if creation was successful, false otherwise. + */ + bool createDescriptorSet(); + + /** + * @brief Create pipeline layout. + * @return True if creation was successful, false otherwise. + */ + bool createPipelineLayout(); + + /** + * @brief Create pipeline. + * @return True if creation was successful, false otherwise. + */ + bool createPipeline(); + + /** + * @brief Update vertex and index buffers. + */ + void updateBuffers(uint32_t frameIndex); }; diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index 6838c93b..f289677c 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -1,14 +1,30 @@ -#include "engine.h" -#include "transform_component.h" +/* 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 "engine.h" #include "scene_loading.h" +#include "transform_component.h" #include #include #include // Constants -constexpr int WINDOW_WIDTH = 800; +constexpr int WINDOW_WIDTH = 800; constexpr int WINDOW_HEIGHT = 600; #if defined(NDEBUG) constexpr bool ENABLE_VALIDATION_LAYERS = false; @@ -16,39 +32,41 @@ constexpr bool ENABLE_VALIDATION_LAYERS = false; 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); - } - std::thread([engine]{ - LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); - }).detach(); +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); + } + std::thread([engine] { + LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + }).detach(); } #if defined(PLATFORM_ANDROID) @@ -56,50 +74,60 @@ void SetupScene(Engine* engine) { * @brief Android entry point. * @param app The Android app. */ -void android_main(android_app* app) { - try { - // Create the engine - Engine engine; - - // Initialize the engine - if (!engine.InitializeAndroid(app, "Simple Engine", ENABLE_VALIDATION_LAYERS)) { - 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()); - } +void android_main(android_app *app) +{ + try + { + // Create the engine + Engine engine; + + // Initialize the engine + if (!engine.InitializeAndroid(app, "Simple Engine", ENABLE_VALIDATION_LAYERS)) + { + 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 { - // Create the engine - Engine engine; - - // Initialize the engine - if (!engine.Initialize("Simple Engine", WINDOW_WIDTH, WINDOW_HEIGHT, ENABLE_VALIDATION_LAYERS)) { - throw std::runtime_error("Failed to initialize engine"); - } - - // Set up the scene - SetupScene(&engine); - - // Run the engine - engine.Run(); - - return 0; - } catch (const std::exception& e) { - std::cerr << "Exception: " << e.what() << std::endl; - return 1; - } +int main(int, char *[]) +{ + try + { + // Create the engine + Engine engine; + + // Initialize the engine + if (!engine.Initialize("Simple Engine", WINDOW_WIDTH, WINDOW_HEIGHT, ENABLE_VALIDATION_LAYERS)) + { + throw std::runtime_error("Failed to initialize engine"); + } + + // Set up the scene + SetupScene(&engine); + + // Run the engine + engine.Run(); + + return 0; + } + catch (const std::exception &e) + { + std::cerr << "Exception: " << e.what() << std::endl; + return 1; + } } #endif diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp index 2e2fbae1..a996e794 100644 --- a/attachments/simple_engine/memory_pool.cpp +++ b/attachments/simple_engine/memory_pool.cpp @@ -1,511 +1,648 @@ +/* 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(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(); +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: Large allocations, device-local (significantly increased for large models like bistro) - configurePool( - PoolType::TEXTURE_IMAGE, - 256 * 1024 * 1024, // 256MB blocks (doubled) - 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; - } +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; + 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(); +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; - } - } + 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"); + throw std::runtime_error("Failed to find suitable memory type"); } -std::unique_ptr MemoryPool::createMemoryBlock(PoolType poolType, vk::DeviceSize size) { - 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 - }; - - // 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::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) { - 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 - }; - - // 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::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::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; +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) { - if (i < block->freeList.size()) { - block->freeList[i] = true; - } - } - - block->used -= allocation->size; - return; - } - } - } - - std::cerr << "Warning: Could not find memory block for deallocation" << std::endl; +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) + { + if (i < block->freeList.size()) + { + 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(); - - // Allocate from pool - auto 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)}; + 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) { - - // Create the image - vk::ImageCreateInfo imageInfo{ - .imageType = vk::ImageType::e2D, - .format = format, - .extent = {width, height, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .samples = vk::SampleCountFlagBits::e1, - .tiling = tiling, - .usage = usage, - .sharingMode = vk::SharingMode::eExclusive, - .initialLayout = vk::ImageLayout::eUndefined - }; - - 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)}; + 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) +{ + // Create the image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = std::max(1u, mipLevels), + .arrayLayers = 1, + .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); +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 poolIt = pools.find(poolType); + if (poolIt == pools.end()) + { + return {0, 0}; + } - vk::DeviceSize used = 0; - vk::DeviceSize total = 0; + vk::DeviceSize used = 0; + vk::DeviceSize total = 0; - for (const auto& block : poolIt->second) { - used += block->used; - total += block->size; - } + for (const auto &block : poolIt->second) + { + used += block->used; + total += block->size; + } - return {used, total}; + return {used, total}; } -std::pair MemoryPool::getTotalMemoryUsage() const { - std::lock_guard lock(poolMutex); +std::pair MemoryPool::getTotalMemoryUsage() const +{ + std::lock_guard lock(poolMutex); - vk::DeviceSize totalUsed = 0; - vk::DeviceSize totalAllocated = 0; + 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; - } - } + for (const auto &[poolType, poolBlocks] : pools) + { + for (const auto &block : poolBlocks) + { + totalUsed += block->used; + totalAllocated += block->size; + } + } - return {totalUsed, totalAllocated}; + 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; - } +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; +void MemoryPool::setRenderingActive(bool active) +{ + std::lock_guard lock(poolMutex); + renderingActive = active; } -bool MemoryPool::isRenderingActive() const { - std::lock_guard lock(poolMutex); - return renderingActive; +bool MemoryPool::isRenderingActive() const +{ + std::lock_guard lock(poolMutex); + return renderingActive; } diff --git a/attachments/simple_engine/memory_pool.h b/attachments/simple_engine/memory_pool.h index c4deeec2..7c3dcaa7 100644 --- a/attachments/simple_engine/memory_pool.h +++ b/attachments/simple_engine/memory_pool.h @@ -1,12 +1,28 @@ +/* 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 /** * @brief Memory pool allocator for Vulkan resources @@ -15,183 +31,187 @@ * 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); - // 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); - 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 - ); - - /** - * @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; +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 = {}); + + /** + * @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/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index c2899e77..39e8b198 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -1,3 +1,19 @@ +/* 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 @@ -5,92 +21,98 @@ // Most of the MeshComponent class implementation is in the header file // This file is mainly for any methods that might need additional implementation -void MeshComponent::CreateSphere(float radius, const glm::vec3& color, int segments) { - vertices.clear(); - indices.clear(); - - // Generate sphere vertices using parametric equations - for (int lat = 0; lat <= segments; ++lat) { - const auto theta = static_cast(lat * M_PI / segments); // Latitude angle (0 to PI) - const float sinTheta = sinf(theta); - const float cosTheta = cosf(theta); - - for (int lon = 0; lon <= segments; ++lon) { - const auto phi = static_cast(lon * 2.0 * M_PI / segments); // Longitude angle (0 to 2*PI) - const float sinPhi = sinf(phi); - const float cosPhi = cosf(phi); - - // Calculate position - glm::vec3 position = { - radius * sinTheta * cosPhi, - radius * cosTheta, - radius * sinTheta * sinPhi - }; - - // Normal is the same as normalized position for a sphere centered at origin - glm::vec3 normal = glm::normalize(position); - - // Texture coordinates - const glm::vec2 texCoord = { - static_cast(lon) / static_cast(segments), - static_cast(lat) / static_cast(segments) - }; - - // Calculate tangent (derivative with respect to longitude). Handle poles robustly. - glm::vec3 tangent = { - -sinTheta * sinPhi, - 0.0f, - sinTheta * cosPhi - }; - float len2 = glm::dot(tangent, tangent); - if (len2 < 1e-12f) { - // At poles sinTheta ~ 0 -> fallback tangent orthogonal to normal - glm::vec3 t = glm::cross(normal, glm::vec3(0.0f, 0.0f, 1.0f)); - if (glm::length(t) < 1e-12f) { - t = glm::cross(normal, glm::vec3(1.0f, 0.0f, 0.0f)); - } - tangent = glm::normalize(t); - } else { - tangent = glm::normalize(tangent); - } - - vertices.push_back({ - position, - normal, - texCoord, - glm::vec4(tangent, 1.0f) - }); - } - } - - // Generate indices for triangles - for (int lat = 0; lat < segments; ++lat) { - for (int lon = 0; lon < segments; ++lon) { - const int current = lat * (segments + 1) + lon; - const int next = current + segments + 1; - - // Create two triangles for each quad - indices.push_back(current); - indices.push_back(next); - indices.push_back(current + 1); - - indices.push_back(current + 1); - indices.push_back(next); - indices.push_back(next + 1); - } - } - - RecomputeLocalAABB(); +void MeshComponent::CreateSphere(float radius, const glm::vec3 &color, int segments) +{ + vertices.clear(); + indices.clear(); + + // Generate sphere vertices using parametric equations + for (int lat = 0; lat <= segments; ++lat) + { + const auto theta = static_cast(lat * M_PI / segments); // Latitude angle (0 to PI) + const float sinTheta = sinf(theta); + const float cosTheta = cosf(theta); + + for (int lon = 0; lon <= segments; ++lon) + { + const auto phi = static_cast(lon * 2.0 * M_PI / segments); // Longitude angle (0 to 2*PI) + const float sinPhi = sinf(phi); + const float cosPhi = cosf(phi); + + // Calculate position + glm::vec3 position = { + radius * sinTheta * cosPhi, + radius * cosTheta, + radius * sinTheta * sinPhi}; + + // Normal is the same as normalized position for a sphere centered at origin + glm::vec3 normal = glm::normalize(position); + + // Texture coordinates + const glm::vec2 texCoord = { + static_cast(lon) / static_cast(segments), + static_cast(lat) / static_cast(segments)}; + + // Calculate tangent (derivative with respect to longitude). Handle poles robustly. + glm::vec3 tangent = { + -sinTheta * sinPhi, + 0.0f, + sinTheta * cosPhi}; + float len2 = glm::dot(tangent, tangent); + if (len2 < 1e-12f) + { + // At poles sinTheta ~ 0 -> fallback tangent orthogonal to normal + glm::vec3 t = glm::cross(normal, glm::vec3(0.0f, 0.0f, 1.0f)); + if (glm::length(t) < 1e-12f) + { + t = glm::cross(normal, glm::vec3(1.0f, 0.0f, 0.0f)); + } + tangent = glm::normalize(t); + } + else + { + tangent = glm::normalize(tangent); + } + + vertices.push_back({position, + normal, + texCoord, + glm::vec4(tangent, 1.0f)}); + } + } + + // Generate indices for triangles + for (int lat = 0; lat < segments; ++lat) + { + for (int lon = 0; lon < segments; ++lon) + { + const int current = lat * (segments + 1) + lon; + const int next = current + segments + 1; + + // Create two triangles for each quad + indices.push_back(current); + indices.push_back(next); + indices.push_back(current + 1); + + indices.push_back(current + 1); + indices.push_back(next); + indices.push_back(next + 1); + } + } + + RecomputeLocalAABB(); } -void MeshComponent::LoadFromModel(const Model* model) { - if (!model) { - return; - } +void MeshComponent::LoadFromModel(const Model *model) +{ + if (!model) + { + return; + } - // Copy vertex and index data from the model - vertices = model->GetVertices(); - indices = model->GetIndices(); + // Copy vertex and index data from the model + vertices = model->GetVertices(); + indices = model->GetIndices(); - RecomputeLocalAABB(); + RecomputeLocalAABB(); } diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index 0f6960eb..db03b535 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -1,9 +1,25 @@ +/* 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 @@ -13,449 +29,499 @@ * @brief Structure representing per-instance data for instanced rendering. * Using explicit float vectors instead of matrices for better control over GPU data layout. */ -struct InstanceData { - // Model matrix as glm::mat4 (4x4) - glm::mat4 modelMatrix{}; - - // Normal matrix as glm::mat3x4 (3 columns of vec4: xyz = normal matrix columns, w unused) - glm::mat3x4 normalMatrix{}; - - InstanceData() { - // Initialize as identity matrices - modelMatrix = glm::mat4(1.0f); - normalMatrix[0] = glm::vec4(1.0f, 0.0f, 0.0f, 0.0f); - normalMatrix[1] = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); - normalMatrix[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); - } - - explicit InstanceData(const glm::mat4& transform, uint32_t matIndex = 0) { - // Store model matrix directly - modelMatrix = transform; - - // Calculate normal matrix (inverse transpose of upper-left 3x3) - glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(transform))); - normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); - normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); - normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); - - // Note: matIndex parameter ignored since materialIndex field was removed - } - - // Helper methods for backward compatibility - [[nodiscard]] glm::mat4 getModelMatrix() const { - return modelMatrix; - } - - void setModelMatrix(const glm::mat4& matrix) { - modelMatrix = matrix; - - // Also update normal matrix when model matrix changes - glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(matrix))); - normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); - normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); - normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); - } - - [[nodiscard]] glm::mat3 getNormalMatrix() const { - return { - glm::vec3(normalMatrix[0]), - glm::vec3(normalMatrix[1]), - glm::vec3(normalMatrix[2]) - }; - } - - - static vk::VertexInputBindingDescription getBindingDescription() { - constexpr vk::VertexInputBindingDescription bindingDescription( - 1, // binding (binding 1 for instance data) - sizeof(InstanceData), // stride - vk::VertexInputRate::eInstance // inputRate - ); - return bindingDescription; - } - - static std::array getAttributeDescriptions() { - constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); - constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); - constexpr uint32_t vec4Size = sizeof(glm::vec4); - constexpr std::array attributeDescriptions = { - // Model matrix columns (locations 4-7) - vk::VertexInputAttributeDescription{ - .location = 4, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 0u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 5, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 1u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 6, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 2u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 7, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 3u * vec4Size - }, - // Normal matrix columns (locations 8-10) - vk::VertexInputAttributeDescription{ - .location = 8, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 0u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 9, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 1u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 10, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 2u * vec4Size - } - }; - return attributeDescriptions; - } - - // Get all attribute descriptions for model matrix (4 vec4s) - static std::array getModelMatrixAttributeDescriptions() { - constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); - constexpr uint32_t vec4Size = sizeof(glm::vec4); - constexpr std::array attributeDescriptions = { - vk::VertexInputAttributeDescription{ - .location = 4, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 0u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 5, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 1u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 6, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 2u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 7, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = modelBase + 3u * vec4Size - } - }; - return attributeDescriptions; - } - - // Get all attribute descriptions for normal matrix (3 vec4s) - static std::array getNormalMatrixAttributeDescriptions() { - constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); - constexpr uint32_t vec4Size = sizeof(glm::vec4); - constexpr std::array attributeDescriptions = { - vk::VertexInputAttributeDescription{ - .location = 8, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 0u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 9, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 1u * vec4Size - }, - vk::VertexInputAttributeDescription{ - .location = 10, - .binding = 1, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = normalBase + 2u * vec4Size - } - }; - return attributeDescriptions; - } +struct InstanceData +{ + // Model matrix as glm::mat4 (4x4) + glm::mat4 modelMatrix{}; + + // Normal matrix as glm::mat3x4 (3 columns of vec4: xyz = normal matrix columns, w unused) + glm::mat3x4 normalMatrix{}; + + InstanceData() + { + // Initialize as identity matrices + modelMatrix = glm::mat4(1.0f); + normalMatrix[0] = glm::vec4(1.0f, 0.0f, 0.0f, 0.0f); + normalMatrix[1] = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); + normalMatrix[2] = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); + } + + explicit InstanceData(const glm::mat4 &transform, uint32_t matIndex = 0) + { + // Store model matrix directly + modelMatrix = transform; + + // Calculate normal matrix (inverse transpose of upper-left 3x3) + glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(transform))); + normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); + normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); + normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); + + // Note: matIndex parameter ignored since materialIndex field was removed + } + + // Helper methods for backward compatibility + [[nodiscard]] glm::mat4 getModelMatrix() const + { + return modelMatrix; + } + + void setModelMatrix(const glm::mat4 &matrix) + { + modelMatrix = matrix; + + // Also update normal matrix when model matrix changes + glm::mat3 normalMat3 = glm::transpose(glm::inverse(glm::mat3(matrix))); + normalMatrix[0] = glm::vec4(normalMat3[0], 0.0f); + normalMatrix[1] = glm::vec4(normalMat3[1], 0.0f); + normalMatrix[2] = glm::vec4(normalMat3[2], 0.0f); + } + + [[nodiscard]] glm::mat3 getNormalMatrix() const + { + return { + glm::vec3(normalMatrix[0]), + glm::vec3(normalMatrix[1]), + glm::vec3(normalMatrix[2])}; + } + + static vk::VertexInputBindingDescription getBindingDescription() + { + constexpr vk::VertexInputBindingDescription bindingDescription( + 1, // binding (binding 1 for instance data) + sizeof(InstanceData), // stride + vk::VertexInputRate::eInstance // inputRate + ); + return bindingDescription; + } + + static std::array getAttributeDescriptions() + { + constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); + constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + constexpr std::array attributeDescriptions = { + // Model matrix columns (locations 4-7) + vk::VertexInputAttributeDescription{ + .location = 4, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 0u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 5, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 1u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 6, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 2u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 7, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 3u * vec4Size}, + // Normal matrix columns (locations 8-10) + vk::VertexInputAttributeDescription{ + .location = 8, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 0u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 9, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 1u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 10, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 2u * vec4Size}}; + return attributeDescriptions; + } + + // Get all attribute descriptions for model matrix (4 vec4s) + static std::array getModelMatrixAttributeDescriptions() + { + constexpr uint32_t modelBase = offsetof(InstanceData, modelMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + constexpr std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 4, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 0u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 5, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 1u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 6, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 2u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 7, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = modelBase + 3u * vec4Size}}; + return attributeDescriptions; + } + + // Get all attribute descriptions for normal matrix (3 vec4s) + static std::array getNormalMatrixAttributeDescriptions() + { + constexpr uint32_t normalBase = offsetof(InstanceData, normalMatrix); + constexpr uint32_t vec4Size = sizeof(glm::vec4); + constexpr std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 8, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 0u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 9, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 1u * vec4Size}, + vk::VertexInputAttributeDescription{ + .location = 10, + .binding = 1, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = normalBase + 2u * vec4Size}}; + return attributeDescriptions; + } }; /** * @brief Structure representing a vertex in a mesh. */ -struct Vertex { - glm::vec3 position; - glm::vec3 normal; - glm::vec2 texCoord; - glm::vec4 tangent; - - bool operator==(const Vertex& other) const { - return position == other.position && - normal == other.normal && - texCoord == other.texCoord && - tangent == other.tangent; - } - - static vk::VertexInputBindingDescription getBindingDescription() { - constexpr vk::VertexInputBindingDescription bindingDescription( - 0, // binding - sizeof(Vertex), // stride - vk::VertexInputRate::eVertex // inputRate - ); - return bindingDescription; - } - - static std::array getAttributeDescriptions() { - constexpr std::array attributeDescriptions = { - vk::VertexInputAttributeDescription{ - .location = 0, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = offsetof(Vertex, position) - }, - vk::VertexInputAttributeDescription{ - .location = 1, - .binding = 0, - .format = vk::Format::eR32G32B32Sfloat, - .offset = offsetof(Vertex, normal) - }, - vk::VertexInputAttributeDescription{ - .location = 2, - .binding = 0, - .format = vk::Format::eR32G32Sfloat, - .offset = offsetof(Vertex, texCoord) - }, - vk::VertexInputAttributeDescription{ - .location = 3, - .binding = 0, - .format = vk::Format::eR32G32B32A32Sfloat, - .offset = offsetof(Vertex, tangent) - } - }; - return attributeDescriptions; - } +struct Vertex +{ + glm::vec3 position; + glm::vec3 normal; + glm::vec2 texCoord; + glm::vec4 tangent; + + bool operator==(const Vertex &other) const + { + return position == other.position && + normal == other.normal && + texCoord == other.texCoord && + tangent == other.tangent; + } + + static vk::VertexInputBindingDescription getBindingDescription() + { + constexpr vk::VertexInputBindingDescription bindingDescription( + 0, // binding + sizeof(Vertex), // stride + vk::VertexInputRate::eVertex // inputRate + ); + return bindingDescription; + } + + static std::array getAttributeDescriptions() + { + constexpr std::array attributeDescriptions = { + vk::VertexInputAttributeDescription{ + .location = 0, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = offsetof(Vertex, position)}, + vk::VertexInputAttributeDescription{ + .location = 1, + .binding = 0, + .format = vk::Format::eR32G32B32Sfloat, + .offset = offsetof(Vertex, normal)}, + vk::VertexInputAttributeDescription{ + .location = 2, + .binding = 0, + .format = vk::Format::eR32G32Sfloat, + .offset = offsetof(Vertex, texCoord)}, + vk::VertexInputAttributeDescription{ + .location = 3, + .binding = 0, + .format = vk::Format::eR32G32B32A32Sfloat, + .offset = offsetof(Vertex, tangent)}}; + return attributeDescriptions; + } }; /** * @brief Component that handles the mesh data for rendering. */ -class MeshComponent final : public Component { -private: - std::vector vertices; - std::vector indices; - - // Cached local-space AABB - glm::vec3 localAABBMin{0.0f}; - glm::vec3 localAABBMax{0.0f}; - bool localAABBValid = false; - - // All PBR texture paths for this mesh - std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility - std::string baseColorTexturePath; // Base color (albedo) texture - std::string normalTexturePath; // Normal map texture - std::string metallicRoughnessTexturePath; // Metallic-roughness texture - std::string occlusionTexturePath; // Ambient occlusion texture - std::string emissiveTexturePath; // Emissive texture - - // Instancing support - std::vector instances; // Instance data for instanced rendering - bool isInstanced = false; // Flag to indicate if this mesh uses instancing - - // The renderer will manage Vulkan resources - // This component only stores the data - -public: - /** - * @brief Constructor with an optional name. - * @param componentName The name of the component. - */ - explicit MeshComponent(const std::string& componentName = "MeshComponent") - : Component(componentName) {} - - // Local AABB utilities - void RecomputeLocalAABB() { - if (vertices.empty()) { - localAABBMin = glm::vec3(0.0f); - localAABBMax = glm::vec3(0.0f); - localAABBValid = false; - return; - } - glm::vec3 minB = vertices[0].position; - glm::vec3 maxB = vertices[0].position; - for (const auto& v : vertices) { - minB = glm::min(minB, v.position); - maxB = glm::max(maxB, v.position); - } - localAABBMin = minB; - localAABBMax = maxB; - localAABBValid = true; - } - [[nodiscard]] bool HasLocalAABB() const { return localAABBValid; } - [[nodiscard]] glm::vec3 GetLocalAABBMin() const { return localAABBMin; } - [[nodiscard]] glm::vec3 GetLocalAABBMax() const { return localAABBMax; } - - /** - * @brief Set the vertices of the mesh. - * @param newVertices The new vertices. - */ - void SetVertices(const std::vector& newVertices) { - vertices = newVertices; - RecomputeLocalAABB(); - } - - /** - * @brief Get the vertices of the mesh. - * @return The vertices. - */ - [[nodiscard]] const std::vector& GetVertices() const { - return vertices; - } - - /** - * @brief Set the indices of the mesh. - * @param newIndices The new indices. - */ - void SetIndices(const std::vector& newIndices) { - indices = newIndices; - } - - /** - * @brief Get the indices of the mesh. - * @return The indices. - */ - [[nodiscard]] const std::vector& GetIndices() const { - return indices; - } - - /** - * @brief Set the texture path for the mesh. - * @param path The path to the texture file. - */ - void SetTexturePath(const std::string& path) { - texturePath = path; - baseColorTexturePath = path; // Keep baseColor in sync for backward compatibility - } - - /** - * @brief Get the texture path for the mesh. - * @return The path to the texture file. - */ - [[nodiscard]] const std::string& GetTexturePath() const { - return texturePath; - } - - // PBR texture path setters - void SetBaseColorTexturePath(const std::string& path) { baseColorTexturePath = path; } - void SetNormalTexturePath(const std::string& path) { normalTexturePath = path; } - void SetMetallicRoughnessTexturePath(const std::string& path) { metallicRoughnessTexturePath = path; } - void SetOcclusionTexturePath(const std::string& path) { occlusionTexturePath = path; } - void SetEmissiveTexturePath(const std::string& path) { emissiveTexturePath = path; } - - // PBR texture path getters - [[nodiscard]] const std::string& GetBaseColorTexturePath() const { return baseColorTexturePath; } - [[nodiscard]] const std::string& GetNormalTexturePath() const { return normalTexturePath; } - [[nodiscard]] const std::string& GetMetallicRoughnessTexturePath() const { return metallicRoughnessTexturePath; } - [[nodiscard]] const std::string& GetOcclusionTexturePath() const { return occlusionTexturePath; } - [[nodiscard]] const std::string& GetEmissiveTexturePath() const { return emissiveTexturePath; } - - /** - * @brief Create a simple sphere mesh. - * @param radius The radius of the sphere. - * @param color The color of the sphere. - * @param segments The number of segments (resolution). - */ - void CreateSphere(float radius = 1.0f, const glm::vec3& color = glm::vec3(1.0f), int segments = 16); - - /** - * @brief Load mesh data from a Model. - * @param model Pointer to the model to load from. - */ - void LoadFromModel(const class Model* model); - - // Instancing methods - - /** - * @brief Add an instance with the given transform matrix. - * @param transform The transform matrix for this instance. - * @param materialIndex The material index for this instance (default: 0). - */ - void AddInstance(const glm::mat4& transform, uint32_t materialIndex = 0) { - instances.emplace_back(transform, materialIndex); - isInstanced = instances.size() > 1; - } - - /** - * @brief Set all instances at once. - * @param newInstances Vector of instance data. - */ - void SetInstances(const std::vector& newInstances) { - instances = newInstances; - isInstanced = instances.size() > 1; - } - - /** - * @brief Get all instance data. - * @return Reference to the instances vector. - */ - [[nodiscard]] const std::vector& GetInstances() const { - return instances; - } - - /** - * @brief Get the number of instances. - * @return Number of instances (0 if not instanced, >= 1 if instanced). - */ - [[nodiscard]] size_t GetInstanceCount() const { - return instances.size(); - } - - /** - * @brief Check if this mesh uses instancing. - * @return True if instanced (more than 1 instance), false otherwise. - */ - [[nodiscard]] bool IsInstanced() const { - return isInstanced; - } - - /** - * @brief Clear all instances and disable instancing. - */ - void ClearInstances() { - instances.clear(); - isInstanced = false; - } - - /** - * @brief Update a specific instance's transform. - * @param index The index of the instance to update. - * @param transform The new transform matrix. - * @param materialIndex The new material index (optional). - */ - void UpdateInstance(size_t index, const glm::mat4& transform, uint32_t materialIndex = 0) { - if (index < instances.size()) { - instances[index] = InstanceData(transform, materialIndex); - } - } - - /** - * @brief Get a specific instance's data. - * @param index The index of the instance. - * @return Reference to the instance data, or first instance if the index is out of bounds. - */ - [[nodiscard]] const InstanceData& GetInstance(size_t index) const { - if (index < instances.size()) { - return instances[index]; - } - // Return the first instance or default if empty - static const InstanceData defaultInstance; - return instances.empty() ? defaultInstance : instances[0]; - } +class MeshComponent final : public Component +{ + private: + std::vector vertices; + std::vector indices; + + // Cached local-space AABB + glm::vec3 localAABBMin{0.0f}; + glm::vec3 localAABBMax{0.0f}; + bool localAABBValid = false; + + // All PBR texture paths for this mesh + std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility + std::string baseColorTexturePath; // Base color (albedo) texture + std::string normalTexturePath; // Normal map texture + std::string metallicRoughnessTexturePath; // Metallic-roughness texture + std::string occlusionTexturePath; // Ambient occlusion texture + std::string emissiveTexturePath; // Emissive texture + + // Instancing support + std::vector instances; // Instance data for instanced rendering + bool isInstanced = false; // Flag to indicate if this mesh uses instancing + + // The renderer will manage Vulkan resources + // This component only stores the data + + public: + /** + * @brief Constructor with an optional name. + * @param componentName The name of the component. + */ + explicit MeshComponent(const std::string &componentName = "MeshComponent") : + Component(componentName) + {} + + // Local AABB utilities + void RecomputeLocalAABB() + { + if (vertices.empty()) + { + localAABBMin = glm::vec3(0.0f); + localAABBMax = glm::vec3(0.0f); + localAABBValid = false; + return; + } + glm::vec3 minB = vertices[0].position; + glm::vec3 maxB = vertices[0].position; + for (const auto &v : vertices) + { + minB = glm::min(minB, v.position); + maxB = glm::max(maxB, v.position); + } + localAABBMin = minB; + localAABBMax = maxB; + localAABBValid = true; + } + [[nodiscard]] bool HasLocalAABB() const + { + return localAABBValid; + } + [[nodiscard]] glm::vec3 GetLocalAABBMin() const + { + return localAABBMin; + } + [[nodiscard]] glm::vec3 GetLocalAABBMax() const + { + return localAABBMax; + } + + /** + * @brief Set the vertices of the mesh. + * @param newVertices The new vertices. + */ + void SetVertices(const std::vector &newVertices) + { + vertices = newVertices; + RecomputeLocalAABB(); + } + + /** + * @brief Get the vertices of the mesh. + * @return The vertices. + */ + [[nodiscard]] const std::vector &GetVertices() const + { + return vertices; + } + + /** + * @brief Set the indices of the mesh. + * @param newIndices The new indices. + */ + void SetIndices(const std::vector &newIndices) + { + indices = newIndices; + } + + /** + * @brief Get the indices of the mesh. + * @return The indices. + */ + [[nodiscard]] const std::vector &GetIndices() const + { + return indices; + } + + /** + * @brief Set the texture path for the mesh. + * @param path The path to the texture file. + */ + void SetTexturePath(const std::string &path) + { + texturePath = path; + baseColorTexturePath = path; // Keep baseColor in sync for backward compatibility + } + + /** + * @brief Get the texture path for the mesh. + * @return The path to the texture file. + */ + [[nodiscard]] const std::string &GetTexturePath() const + { + return texturePath; + } + + // PBR texture path setters + void SetBaseColorTexturePath(const std::string &path) + { + baseColorTexturePath = path; + } + void SetNormalTexturePath(const std::string &path) + { + normalTexturePath = path; + } + void SetMetallicRoughnessTexturePath(const std::string &path) + { + metallicRoughnessTexturePath = path; + } + void SetOcclusionTexturePath(const std::string &path) + { + occlusionTexturePath = path; + } + void SetEmissiveTexturePath(const std::string &path) + { + emissiveTexturePath = path; + } + + // PBR texture path getters + [[nodiscard]] const std::string &GetBaseColorTexturePath() const + { + return baseColorTexturePath; + } + [[nodiscard]] const std::string &GetNormalTexturePath() const + { + return normalTexturePath; + } + [[nodiscard]] const std::string &GetMetallicRoughnessTexturePath() const + { + return metallicRoughnessTexturePath; + } + [[nodiscard]] const std::string &GetOcclusionTexturePath() const + { + return occlusionTexturePath; + } + [[nodiscard]] const std::string &GetEmissiveTexturePath() const + { + return emissiveTexturePath; + } + + /** + * @brief Create a simple sphere mesh. + * @param radius The radius of the sphere. + * @param color The color of the sphere. + * @param segments The number of segments (resolution). + */ + void CreateSphere(float radius = 1.0f, const glm::vec3 &color = glm::vec3(1.0f), int segments = 16); + + /** + * @brief Load mesh data from a Model. + * @param model Pointer to the model to load from. + */ + void LoadFromModel(const class Model *model); + + // Instancing methods + + /** + * @brief Add an instance with the given transform matrix. + * @param transform The transform matrix for this instance. + * @param materialIndex The material index for this instance (default: 0). + */ + void AddInstance(const glm::mat4 &transform, uint32_t materialIndex = 0) + { + instances.emplace_back(transform, materialIndex); + isInstanced = instances.size() > 1; + } + + /** + * @brief Set all instances at once. + * @param newInstances Vector of instance data. + */ + void SetInstances(const std::vector &newInstances) + { + instances = newInstances; + isInstanced = instances.size() > 1; + } + + /** + * @brief Get all instance data. + * @return Reference to the instances vector. + */ + [[nodiscard]] const std::vector &GetInstances() const + { + return instances; + } + + /** + * @brief Get the number of instances. + * @return Number of instances (0 if not instanced, >= 1 if instanced). + */ + [[nodiscard]] size_t GetInstanceCount() const + { + return instances.size(); + } + + /** + * @brief Check if this mesh uses instancing. + * @return True if instanced (more than 1 instance), false otherwise. + */ + [[nodiscard]] bool IsInstanced() const + { + return isInstanced; + } + + /** + * @brief Clear all instances and disable instancing. + */ + void ClearInstances() + { + instances.clear(); + isInstanced = false; + } + + /** + * @brief Update a specific instance's transform. + * @param index The index of the instance to update. + * @param transform The new transform matrix. + * @param materialIndex The new material index (optional). + */ + void UpdateInstance(size_t index, const glm::mat4 &transform, uint32_t materialIndex = 0) + { + if (index < instances.size()) + { + instances[index] = InstanceData(transform, materialIndex); + } + } + + /** + * @brief Get a specific instance's data. + * @param index The index of the instance. + * @return Reference to the instance data, or first instance if the index is out of bounds. + */ + [[nodiscard]] const InstanceData &GetInstance(size_t index) const + { + if (index < instances.size()) + { + return instances[index]; + } + // Return the first instance or default if empty + static const InstanceData defaultInstance; + return instances.empty() ? defaultInstance : instances[0]; + } }; diff --git a/attachments/simple_engine/mikktspace.h b/attachments/simple_engine/mikktspace.h index 52c44a71..e3929e9a 100644 --- a/attachments/simple_engine/mikktspace.h +++ b/attachments/simple_engine/mikktspace.h @@ -24,119 +24,118 @@ #ifndef __MIKKTSPACE_H__ #define __MIKKTSPACE_H__ - #ifdef __cplusplus -extern "C" { -#endif - -/* Author: Morten S. Mikkelsen - * Version: 1.0 - * - * The files mikktspace.h and mikktspace.c are designed to be - * stand-alone files and it is important that they are kept this way. - * Not having dependencies on structures/classes/libraries specific - * to the program, in which they are used, allows them to be copied - * and used as is into any tool, program or plugin. - * The code is designed to consistently generate the same - * tangent spaces, for a given mesh, in any tool in which it is used. - * This is done by performing an internal welding step and subsequently an order-independent evaluation - * of tangent space for meshes consisting of triangles and quads. - * This means faces can be received in any order and the same is true for - * the order of vertices of each face. The generated result will not be affected - * by such reordering. Additionally, whether degenerate (vertices or texture coordinates) - * primitives are present or not will not affect the generated results either. - * Once tangent space calculation is done the vertices of degenerate primitives will simply - * inherit tangent space from neighboring non degenerate primitives. - * The analysis behind this implementation can be found in my master's thesis - * which is available for download --> http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf - * Note that though the tangent spaces at the vertices are generated in an order-independent way, - * by this implementation, the interpolated tangent space is still affected by which diagonal is - * chosen to split each quad. A sensible solution is to have your tools pipeline always - * split quads by the shortest diagonal. This choice is order-independent and works with mirroring. - * If these have the same length then compare the diagonals defined by the texture coordinates. - * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin - * and also quad triangulator plugin. - */ - - -typedef int tbool; -typedef struct SMikkTSpaceContext SMikkTSpaceContext; - -typedef struct { - // Returns the number of faces (triangles/quads) on the mesh to be processed. - int (*m_getNumFaces)(const SMikkTSpaceContext * pContext); - - // Returns the number of vertices on face number iFace - // iFace is a number in the range {0, 1, ..., getNumFaces()-1} - int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace); - - // returns the position/normal/texcoord of the referenced face of vertex number iVert. - // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. - void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert); - void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert); - void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert); - - // either (or both) of the two setTSpace callbacks can be set. - // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. - - // This function is used to return the tangent and fSign to the application. - // fvTangent is a unit length vector. - // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. - // bitangent = fSign * cross(vN, tangent); - // Note that the results are returned unindexed. It is possible to generate a new index list - // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. - // DO NOT! use an already existing index list. - void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert); - - // This function is used to return tangent space results to the application. - // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their - // true magnitudes which can be used for relief mapping effects. - // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. - // However, both are perpendicular to the vertex normal. - // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. - // fSign = bIsOrientationPreserving ? 1.0f : (-1.0f); - // bitangent = fSign * cross(vN, tangent); - // Note that the results are returned unindexed. It is possible to generate a new index list - // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. - // DO NOT! use an already existing index list. - void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT, - const tbool bIsOrientationPreserving, const int iFace, const int iVert); -} SMikkTSpaceInterface; - -struct SMikkTSpaceContext +extern "C" { - SMikkTSpaceInterface * m_pInterface; // initialized with callback functions - void * m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call) -}; - -// these are both thread safe! -tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) -tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold); - +#endif -// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the -// normal map sampler must use the exact inverse of the pixel shader transformation. -// The most efficient transformation we can possibly do in the pixel shader is -// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN. -// pixel shader (fast transform out) -// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN ); -// where vNt is the tangent space normal. The normal map sampler must likewise use the -// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader. -// sampler does (exact inverse of pixel shader): -// float3 row0 = cross(vB, vN); -// float3 row1 = cross(vN, vT); -// float3 row2 = cross(vT, vB); -// float fSign = dot(vT, row0)<0 ? -1 : 1; -// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) ); -// where vNout is the sampled normal in some chosen 3D space. -// -// Should you choose to reconstruct the bitangent in the pixel shader instead -// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also. -// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of -// quads as your renderer then problems will occur since the interpolated tangent spaces will differ -// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before -// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier. -// However, this must be used both by the sampler and your tools/rendering pipeline. + /* Author: Morten S. Mikkelsen + * Version: 1.0 + * + * The files mikktspace.h and mikktspace.c are designed to be + * stand-alone files and it is important that they are kept this way. + * Not having dependencies on structures/classes/libraries specific + * to the program, in which they are used, allows them to be copied + * and used as is into any tool, program or plugin. + * The code is designed to consistently generate the same + * tangent spaces, for a given mesh, in any tool in which it is used. + * This is done by performing an internal welding step and subsequently an order-independent evaluation + * of tangent space for meshes consisting of triangles and quads. + * This means faces can be received in any order and the same is true for + * the order of vertices of each face. The generated result will not be affected + * by such reordering. Additionally, whether degenerate (vertices or texture coordinates) + * primitives are present or not will not affect the generated results either. + * Once tangent space calculation is done the vertices of degenerate primitives will simply + * inherit tangent space from neighboring non degenerate primitives. + * The analysis behind this implementation can be found in my master's thesis + * which is available for download --> http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf + * Note that though the tangent spaces at the vertices are generated in an order-independent way, + * by this implementation, the interpolated tangent space is still affected by which diagonal is + * chosen to split each quad. A sensible solution is to have your tools pipeline always + * split quads by the shortest diagonal. This choice is order-independent and works with mirroring. + * If these have the same length then compare the diagonals defined by the texture coordinates. + * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin + * and also quad triangulator plugin. + */ + + typedef int tbool; + typedef struct SMikkTSpaceContext SMikkTSpaceContext; + + typedef struct + { + // Returns the number of faces (triangles/quads) on the mesh to be processed. + int (*m_getNumFaces)(const SMikkTSpaceContext *pContext); + + // Returns the number of vertices on face number iFace + // iFace is a number in the range {0, 1, ..., getNumFaces()-1} + int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext *pContext, const int iFace); + + // returns the position/normal/texcoord of the referenced face of vertex number iVert. + // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. + void (*m_getPosition)(const SMikkTSpaceContext *pContext, float fvPosOut[], const int iFace, const int iVert); + void (*m_getNormal)(const SMikkTSpaceContext *pContext, float fvNormOut[], const int iFace, const int iVert); + void (*m_getTexCoord)(const SMikkTSpaceContext *pContext, float fvTexcOut[], const int iFace, const int iVert); + + // either (or both) of the two setTSpace callbacks can be set. + // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. + + // This function is used to return the tangent and fSign to the application. + // fvTangent is a unit length vector. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpaceBasic)(const SMikkTSpaceContext *pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert); + + // This function is used to return tangent space results to the application. + // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their + // true magnitudes which can be used for relief mapping effects. + // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. + // However, both are perpendicular to the vertex normal. + // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. + // fSign = bIsOrientationPreserving ? 1.0f : (-1.0f); + // bitangent = fSign * cross(vN, tangent); + // Note that the results are returned unindexed. It is possible to generate a new index list + // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. + // DO NOT! use an already existing index list. + void (*m_setTSpace)(const SMikkTSpaceContext *pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT, + const tbool bIsOrientationPreserving, const int iFace, const int iVert); + } SMikkTSpaceInterface; + + struct SMikkTSpaceContext + { + SMikkTSpaceInterface *m_pInterface; // initialized with callback functions + void *m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call) + }; + + // these are both thread safe! + tbool genTangSpaceDefault(const SMikkTSpaceContext *pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) + tbool genTangSpace(const SMikkTSpaceContext *pContext, const float fAngularThreshold); + + // To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the + // normal map sampler must use the exact inverse of the pixel shader transformation. + // The most efficient transformation we can possibly do in the pixel shader is + // achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN. + // pixel shader (fast transform out) + // vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN ); + // where vNt is the tangent space normal. The normal map sampler must likewise use the + // interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader. + // sampler does (exact inverse of pixel shader): + // float3 row0 = cross(vB, vN); + // float3 row1 = cross(vN, vT); + // float3 row2 = cross(vT, vB); + // float fSign = dot(vT, row0)<0 ? -1 : 1; + // vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) ); + // where vNout is the sampled normal in some chosen 3D space. + // + // Should you choose to reconstruct the bitangent in the pixel shader instead + // of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also. + // Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of + // quads as your renderer then problems will occur since the interpolated tangent spaces will differ + // eventhough the vertex level tangent spaces match. This can be solved either by triangulating before + // sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier. + // However, this must be used both by the sampler and your tools/rendering pipeline. #ifdef __cplusplus } diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 9cf9ec1f..8ce54e55 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -1,1745 +1,2345 @@ +/* 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 "model_loader.h" -#include "renderer.h" #include "mesh_component.h" -#include -#include -#include +#include "renderer.h" #include #include +#include +#include +#include #include #include "mikktspace.h" // This struct acts as a bridge between the C-style MikkTSpace callbacks // and our C++ MaterialMesh vertex data. It's passed via the m_pUserData pointer. -struct MikkTSpaceInterface { - std::vector* vertices; - std::vector* indices; +struct MikkTSpaceInterface +{ + std::vector *vertices; + std::vector *indices; }; // These static callback functions are required by the MikkTSpace library. // They are defined here at file-scope so they are not part of the ModelLoader class. -static int getNumFaces(const SMikkTSpaceContext* pContext) { - auto* userData = static_cast(pContext->m_pUserData); - return static_cast(userData->indices->size() / 3); +static int getNumFaces(const SMikkTSpaceContext *pContext) +{ + auto *userData = static_cast(pContext->m_pUserData); + return static_cast(userData->indices->size() / 3); } -static int getNumVerticesOfFace(const SMikkTSpaceContext* pContext, const int iFace) { - return 3; +static int getNumVerticesOfFace(const SMikkTSpaceContext *pContext, const int iFace) +{ + return 3; } -static void getPosition(const SMikkTSpaceContext* pContext, float fvPosOut[], const int iFace, const int iVert) { - auto* userData = static_cast(pContext->m_pUserData); - uint32_t index = (*userData->indices)[iFace * 3 + iVert]; - const glm::vec3& pos = (*userData->vertices)[index].position; - fvPosOut[0] = pos.x; - fvPosOut[1] = pos.y; - fvPosOut[2] = pos.z; +static void getPosition(const SMikkTSpaceContext *pContext, float fvPosOut[], const int iFace, const int iVert) +{ + auto *userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec3 &pos = (*userData->vertices)[index].position; + fvPosOut[0] = pos.x; + fvPosOut[1] = pos.y; + fvPosOut[2] = pos.z; } -static void getNormal(const SMikkTSpaceContext* pContext, float fvNormOut[], const int iFace, const int iVert) { - auto* userData = static_cast(pContext->m_pUserData); - uint32_t index = (*userData->indices)[iFace * 3 + iVert]; - const glm::vec3& norm = (*userData->vertices)[index].normal; - fvNormOut[0] = norm.x; - fvNormOut[1] = norm.y; - fvNormOut[2] = norm.z; +static void getNormal(const SMikkTSpaceContext *pContext, float fvNormOut[], const int iFace, const int iVert) +{ + auto *userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec3 &norm = (*userData->vertices)[index].normal; + fvNormOut[0] = norm.x; + fvNormOut[1] = norm.y; + fvNormOut[2] = norm.z; } -static void getTexCoord(const SMikkTSpaceContext* pContext, float fvTexcOut[], const int iFace, const int iVert) { - auto* userData = static_cast(pContext->m_pUserData); - uint32_t index = (*userData->indices)[iFace * 3 + iVert]; - const glm::vec2& uv = (*userData->vertices)[index].texCoord; - fvTexcOut[0] = uv.x; - fvTexcOut[1] = uv.y; +static void getTexCoord(const SMikkTSpaceContext *pContext, float fvTexcOut[], const int iFace, const int iVert) +{ + auto *userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + const glm::vec2 &uv = (*userData->vertices)[index].texCoord; + fvTexcOut[0] = uv.x; + fvTexcOut[1] = uv.y; } -static void setTSpaceBasic(const SMikkTSpaceContext* pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert) { - auto* userData = static_cast(pContext->m_pUserData); - uint32_t index = (*userData->indices)[iFace * 3 + iVert]; - Vertex& vert = (*userData->vertices)[index]; - vert.tangent.x = fvTangent[0]; - vert.tangent.y = fvTangent[1]; - vert.tangent.z = fvTangent[2]; - // Clamp handedness to +/-1 to avoid tiny floating deviations - vert.tangent.w = (fSign >= 0.0f) ? 1.0f : -1.0f; +static void setTSpaceBasic(const SMikkTSpaceContext *pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert) +{ + auto *userData = static_cast(pContext->m_pUserData); + uint32_t index = (*userData->indices)[iFace * 3 + iVert]; + Vertex &vert = (*userData->vertices)[index]; + vert.tangent.x = fvTangent[0]; + vert.tangent.y = fvTangent[1]; + vert.tangent.z = fvTangent[2]; + // Clamp handedness to +/-1 to avoid tiny floating deviations + vert.tangent.w = (fSign >= 0.0f) ? 1.0f : -1.0f; } // KTX2 decoding for GLTF images #include // Helper: load KTX2 file from disk into RGBA8 CPU buffer -static bool LoadKTX2FileToRGBA(const std::string& filePath, std::vector& outData, int& width, int& height, int& channels) { - ktxTexture2* ktxTex = nullptr; - KTX_error_code result = ktxTexture2_CreateFromNamedFile(filePath.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); - if (result != KTX_SUCCESS || !ktxTex) { - return false; - } - bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); - if (needsTranscode) { - result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); - if (result != KTX_SUCCESS) { - ktxTexture_Destroy((ktxTexture*)ktxTex); - return false; - } - } - width = static_cast(ktxTex->baseWidth); - height = static_cast(ktxTex->baseHeight); - channels = 4; - ktx_size_t offset; - ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); - const uint8_t* levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; - size_t levelSize = needsTranscode ? static_cast(width) * static_cast(height) * 4 - : ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); - outData.resize(levelSize); - std::memcpy(outData.data(), levelData, levelSize); - ktxTexture_Destroy((ktxTexture*)ktxTex); - return true; +static bool LoadKTX2FileToRGBA(const std::string &filePath, std::vector &outData, int &width, int &height, int &channels) +{ + ktxTexture2 *ktxTex = nullptr; + KTX_error_code result = ktxTexture2_CreateFromNamedFile(filePath.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result != KTX_SUCCESS || !ktxTex) + { + return false; + } + bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); + if (needsTranscode) + { + result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); + if (result != KTX_SUCCESS) + { + ktxTexture_Destroy((ktxTexture *) ktxTex); + return false; + } + } + width = static_cast(ktxTex->baseWidth); + height = static_cast(ktxTex->baseHeight); + channels = 4; + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture *) ktxTex, 0, 0, 0, &offset); + const uint8_t *levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; + size_t levelSize = needsTranscode ? static_cast(width) * static_cast(height) * 4 : ktxTexture_GetImageSize((ktxTexture *) ktxTex, 0); + outData.resize(levelSize); + std::memcpy(outData.data(), levelData, levelSize); + ktxTexture_Destroy((ktxTexture *) ktxTex); + return true; } // Emissive scaling factor to convert from Blender units to engine units #define EMISSIVE_SCALE_FACTOR (1.0f / 638.0f) #define LIGHT_SCALE_FACTOR (1.0f / 638.0f) -ModelLoader::~ModelLoader() { - // Destructor implementation - models.clear(); - materials.clear(); +ModelLoader::~ModelLoader() +{ + // Destructor implementation + models.clear(); + materials.clear(); } -bool ModelLoader::Initialize(Renderer* _renderer) { - renderer = _renderer; +bool ModelLoader::Initialize(Renderer *_renderer) +{ + renderer = _renderer; - if (!renderer) { - std::cerr << "ModelLoader::Initialize: Renderer is null" << std::endl; - return false; - } + if (!renderer) + { + std::cerr << "ModelLoader::Initialize: Renderer is null" << std::endl; + return false; + } - return true; + return true; } -Model* ModelLoader::LoadGLTF(const std::string& filename) { - // Check if the model is already loaded - auto it = models.find(filename); - if (it != models.end()) { - return it->second.get(); - } - - // Create a new model - auto model = std::make_unique(filename); - - // Parse the GLTF file - if (!ParseGLTF(filename, model.get())) { - std::cerr << "ModelLoader::LoadGLTF: Failed to parse GLTF file: " << filename << std::endl; - return nullptr; - } - - // Store the model - models[filename] = std::move(model); - - return models[filename].get(); +Model *ModelLoader::LoadGLTF(const std::string &filename) +{ + // Check if the model is already loaded + auto it = models.find(filename); + if (it != models.end()) + { + return it->second.get(); + } + + // Create a new model + auto model = std::make_unique(filename); + + // Parse the GLTF file + if (!ParseGLTF(filename, model.get())) + { + std::cerr << "ModelLoader::LoadGLTF: Failed to parse GLTF file: " << filename << std::endl; + return nullptr; + } + + // Store the model + models[filename] = std::move(model); + + return models[filename].get(); } - -Model* ModelLoader::GetModel(const std::string& name) { - auto it = models.find(name); - if (it != models.end()) { - return it->second.get(); - } - return nullptr; +Model *ModelLoader::GetModel(const std::string &name) +{ + auto it = models.find(name); + if (it != models.end()) + { + return it->second.get(); + } + return nullptr; } - -bool ModelLoader::ParseGLTF(const std::string& filename, Model* model) { - std::cout << "Parsing GLTF file: " << filename << std::endl; - - // Extract the directory path from the model file to use as a base path for textures - std::filesystem::path modelPath(filename); - std::filesystem::path baseDir = std::filesystem::absolute(modelPath).parent_path(); - std::string baseTexturePath = baseDir.string(); - if (!baseTexturePath.empty() && baseTexturePath.back() != '/') { - baseTexturePath += "/"; - } - std::cout << "Using base texture path: " << baseTexturePath << std::endl; - - // Create tinygltf loader - tinygltf::Model gltfModel; - tinygltf::TinyGLTF loader; - std::string err; - std::string warn; - - // Set up image loader: prefer KTX2 via libktx; fallback to stb for other formats - loader.SetImageLoader([](tinygltf::Image* image, const int image_idx, std::string* err, - std::string* warn, int req_width, int req_height, - const unsigned char* bytes, int size, void* user_data) -> bool { - // Try KTX2 first using libktx - ktxTexture2* ktxTex = nullptr; - KTX_error_code result = ktxTexture2_CreateFromMemory(bytes, size, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); - if (result == KTX_SUCCESS && ktxTex) { - bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); - if (needsTranscode) { - result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); - if (result != KTX_SUCCESS) { - if (err) *err = "Failed to transcode KTX2 image: " + std::to_string(result); - ktxTexture_Destroy((ktxTexture*)ktxTex); - return false; - } - } - image->width = static_cast(ktxTex->baseWidth); - image->height = static_cast(ktxTex->baseHeight); - image->component = 4; - image->bits = 8; - image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; - - ktx_size_t offset; - ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); - const uint8_t* levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; - size_t levelSize = needsTranscode ? static_cast(image->width) * static_cast(image->height) * 4 - : ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); - image->image.resize(levelSize); - std::memcpy(image->image.data(), levelData, levelSize); - ktxTexture_Destroy((ktxTexture*)ktxTex); - return true; - } - - // Non-KTX images not supported by this loader per project simplification - if (err) { - *err = "Non-KTX2 images are not supported by the custom image loader (use KTX2)."; - } - return false; - }, nullptr); - - // Load the GLTF file - bool ret = false; - if (filename.find(".glb") != std::string::npos) { - ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, filename); - } else { - ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, filename); - } - - if (!warn.empty()) { - std::cout << "GLTF Warning: " << warn << std::endl; - } - - if (!err.empty()) { - std::cerr << "GLTF Error: " << err << std::endl; - return false; - } - - if (!ret) { - std::cerr << "Failed to parse GLTF file: " << filename << std::endl; - return false; - } - - // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) - if (gltfModel.meshes.empty()) { - std::cerr << "No meshes found in GLTF file" << std::endl; - return false; - } - - light_scale = 1.0f; - // Test if generator is blender and apply the blender factor see the issue here: https://github.com/KhronosGroup/glTF/issues/2473 - if (gltfModel.asset.generator.find("blender") != std::string::npos) { - std::cout << "Blender generator detected, applying blender factor" << std::endl; - light_scale = EMISSIVE_SCALE_FACTOR; - } - - // Track loaded textures to prevent loading the same texture multiple times - std::set loadedTextures; - - // Helper: lowercase a std::string (ASCII only) - auto toLower = [](const std::string& s) { - std::string out = s; - std::ranges::transform(out, out.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return out; - }; - - // Process materials first - for (size_t i = 0; i < gltfModel.materials.size(); ++i) { - const auto& gltfMaterial = gltfModel.materials[i]; - - // Create PBR material - auto material = std::make_unique(gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name); - - // Extract PBR properties - if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 3) { - material->albedo = glm::vec3( - gltfMaterial.pbrMetallicRoughness.baseColorFactor[0], - gltfMaterial.pbrMetallicRoughness.baseColorFactor[1], - gltfMaterial.pbrMetallicRoughness.baseColorFactor[2] - ); - if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 4) { - material->alpha = static_cast(gltfMaterial.pbrMetallicRoughness.baseColorFactor[3]); - } - } - material->metallic = static_cast(gltfMaterial.pbrMetallicRoughness.metallicFactor); - material->roughness = static_cast(gltfMaterial.pbrMetallicRoughness.roughnessFactor); - - if (gltfMaterial.emissiveFactor.size() >= 3) { - material->emissive = glm::vec3( - gltfMaterial.emissiveFactor[0], - gltfMaterial.emissiveFactor[1], - gltfMaterial.emissiveFactor[2] - ); - material->emissive *= light_scale; - } - - // Parse KHR_materials_emissive_strength extension - auto extensionIt = gltfMaterial.extensions.find("KHR_materials_emissive_strength"); - if (extensionIt != gltfMaterial.extensions.end()) { - hasEmissiveStrengthExtension = true; - const tinygltf::Value& extension = extensionIt->second; - if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) { - material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()); - } - } else { - material->emissiveStrength = 0.00058f; - } - - // Alpha mode / cutoff - material->alphaMode = gltfMaterial.alphaMode.empty() ? std::string("OPAQUE") : gltfMaterial.alphaMode; - material->alphaCutoff = static_cast(gltfMaterial.alphaCutoff); - - // Transmission (KHR_materials_transmission) - auto transIt = gltfMaterial.extensions.find("KHR_materials_transmission"); - if (transIt != gltfMaterial.extensions.end()) { - const tinygltf::Value& ext = transIt->second; - if (ext.Has("transmissionFactor") && ext.Get("transmissionFactor").IsNumber()) { - material->transmissionFactor = static_cast(ext.Get("transmissionFactor").Get()); - } - } - - // Classify obvious architectural glass and liquid materials for - // specialized rendering. This is a heuristic based primarily on - // material name. - { - std::string lowerName = toLower(material->GetName()); - bool nameSuggestsGlass = - (lowerName.find("glass") != std::string::npos) || - (lowerName.find("window") != std::string::npos); - - bool probablyLiquid = - (lowerName.find("beer") != std::string::npos) || - (lowerName.find("wine") != std::string::npos) || - (lowerName.find("liquid") != std::string::npos); - - if (nameSuggestsGlass && !probablyLiquid) { - material->isGlass = true; - } - - if (probablyLiquid) { - material->isLiquid = true; - - // Slightly boost liquid visibility. - material->albedo *= 1.4f; - material->albedo = glm::clamp(material->albedo, glm::vec3(0.0f), glm::vec3(4.0f)); - - // Slightly reduce roughness so specular highlights from - // lights help liquids stand out. - material->roughness = glm::clamp(material->roughness * 0.8f, 0.0f, 1.0f); - - // Ensure the liquid is not fully transparent by default. - material->alpha = glm::clamp(material->alpha * 1.2f, 0.15f, 1.0f); - } - } - - // Specular-Glossiness (KHR_materials_pbrSpecularGlossiness) - auto sgIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); - if (sgIt != gltfMaterial.extensions.end()) { - const tinygltf::Value& ext = sgIt->second; - material->useSpecularGlossiness = true; - // diffuseFactor -> albedo and alpha - if (ext.Has("diffuseFactor") && ext.Get("diffuseFactor").IsArray()) { - const auto& arr = ext.Get("diffuseFactor").Get(); - if (arr.size() >= 3) { - material->albedo = glm::vec3( - arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->albedo.r, - arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->albedo.g, - arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->albedo.b - ); - if (arr.size() >= 4 && arr[3].IsNumber()) { - material->alpha = static_cast(arr[3].Get()); - } - } - } - // specularFactor (vec3) - if (ext.Has("specularFactor") && ext.Get("specularFactor").IsArray()) { - const auto& arr = ext.Get("specularFactor").Get(); - if (arr.size() >= 3) { - material->specularFactor = glm::vec3( - arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->specularFactor.r, - arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->specularFactor.g, - arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->specularFactor.b - ); - } - } - // glossinessFactor (float) - if (ext.Has("glossinessFactor") && ext.Get("glossinessFactor").IsNumber()) { - material->glossinessFactor = static_cast(ext.Get("glossinessFactor").Get()); - } - - // Load diffuseTexture into albedoTexturePath if present - if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) { - const auto& diffObj = ext.Get("diffuseTexture"); - if (diffObj.Has("index") && diffObj.Get("index").IsInt()) { - int texIndex = diffObj.Get("index").Get(); - if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { - const auto& texture = gltfModel.textures[texIndex]; - int imageIndex = -1; - if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { - imageIndex = texture.source; - } else { - auto extBasis = texture.extensions.find("KHR_texture_basisu"); - if (extBasis != texture.extensions.end()) { - const tinygltf::Value &e = extBasis->second; - if (e.Has("source") && e.Get("source").IsInt()) { - int src = e.Get("source").Get(); - if (src >= 0 && src < static_cast(gltfModel.images.size())) imageIndex = src; - } - } - } - if (imageIndex >= 0) { - const auto& image = gltfModel.images[imageIndex]; - std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - material->albedoTexturePath = textureId; - } else if (!image.uri.empty()) { - std::string filePath = baseTexturePath + image.uri; - renderer->LoadTextureAsync(filePath); - material->albedoTexturePath = filePath; - } - } - } - } - } - // Load specularGlossinessTexture into specGlossTexturePath and mirror to metallicRoughnessTexturePath (binding 2) - if (ext.Has("specularGlossinessTexture") && ext.Get("specularGlossinessTexture").IsObject()) { - const auto& sgObj = ext.Get("specularGlossinessTexture"); - if (sgObj.Has("index") && sgObj.Get("index").IsInt()) { - int texIndex = sgObj.Get("index").Get(); - if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { - std::string textureId = "gltf_specGloss_" + std::to_string(texIndex); - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Embedded image data (already decoded by tinygltf image loader) - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, false); - material->specGlossTexturePath = textureId; - material->metallicRoughnessTexturePath = textureId; // reuse binding 2 - } else if (!image.uri.empty()) { - // External KTX2 file: offload libktx decode + upload to renderer worker threads - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - material->specGlossTexturePath = textureId; - material->metallicRoughnessTexturePath = textureId; // reuse binding 2 - } - } - } - } - } - } - - // Extract texture information and load embedded texture data - if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - int imageIndex = -1; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - imageIndex = texture.source; - } else { - auto extIt = texture.extensions.find("KHR_texture_basisu"); - if (extIt != texture.extensions.end()) { - const tinygltf::Value& ext = extIt->second; - if (ext.Has("source") && ext.Get("source").IsInt()) { - int src = ext.Get("source").Get(); - if (src >= 0 && src < static_cast(gltfModel.images.size())) { - imageIndex = src; - } - } - } - } - if (imageIndex >= 0) { - std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); - material->albedoTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[imageIndex]; - std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; - if (!image.image.empty()) { - // Always use memory-based upload (KTX2 already decoded by SetImageLoader) - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); - material->albedoTexturePath = textureId; - std::cout << " Scheduled base color texture upload from memory: " << textureId << std::endl; - } else if (!image.uri.empty()) { - // Offload KTX2 file reading/upload to renderer thread pool - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath, true); - material->albedoTexturePath = textureId; - std::cout << " Scheduled base color KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded image bytes for base color texture index " << texIndex << std::endl; - } - } - } - } - - if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - material->metallicRoughnessTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Load embedded texture data asynchronously - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded metallic-roughness texture upload: " << textureId << std::endl; - } else if (!image.uri.empty()) { - // Offload KTX2 file reading/upload to renderer thread pool - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - material->metallicRoughnessTexturePath = textureId; - std::cout << " Scheduled metallic-roughness KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; - } - } - } - } - - if (gltfMaterial.normalTexture.index >= 0) { - int texIndex = gltfMaterial.normalTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - int imageIndex = -1; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - imageIndex = texture.source; - } else { - auto extIt = texture.extensions.find("KHR_texture_basisu"); - if (extIt != texture.extensions.end()) { - const tinygltf::Value& ext = extIt->second; - if (ext.Has("source") && ext.Get("source").IsInt()) { - int src = ext.Get("source").Get(); - if (src >= 0 && src < static_cast(gltfModel.images.size())) { - imageIndex = src; - } - } - } - } - if (imageIndex >= 0) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - material->normalTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[imageIndex]; - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - material->normalTexturePath = textureId; - std::cout << " Scheduled normal texture upload from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else if (!image.uri.empty()) { - // Offload KTX2 file reading/upload to renderer thread pool - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - material->normalTexturePath = textureId; - std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; - } - } - } - } - - if (gltfMaterial.occlusionTexture.index >= 0) { - int texIndex = gltfMaterial.occlusionTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - material->occlusionTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Schedule embedded texture upload - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded occlusion texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else if (!image.uri.empty()) { - // Offload KTX2 file reading/upload to renderer thread pool - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - material->occlusionTexturePath = textureId; - std::cout << " Scheduled occlusion KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; - } - } - } - } - - if (gltfMaterial.emissiveTexture.index >= 0) { - int texIndex = gltfMaterial.emissiveTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - material->emissiveTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Schedule embedded texture upload - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded emissive texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else if (!image.uri.empty()) { - // Offload KTX2 file reading/upload to renderer thread pool - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - material->emissiveTexturePath = textureId; - std::cout << " Scheduled emissive KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for emissive texture index " << texIndex << std::endl; - } - } - } - } - - // Store the material - materials[material->GetName()] = std::move(material); - } - - // Handle KHR_materials_pbrSpecularGlossiness.diffuseTexture for baseColor when still missing - for (size_t i = 0; i < gltfModel.materials.size(); ++i) { - const auto &gltfMaterial = gltfModel.materials[i]; - std::string matName = gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name; - auto matIt = materials.find(matName); - if (matIt == materials.end()) continue; - Material* mat = matIt->second.get(); - if (!mat || !mat->albedoTexturePath.empty()) continue; - auto extIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); - if (extIt != gltfMaterial.extensions.end()) { - const tinygltf::Value &ext = extIt->second; - if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) { - const auto &diffObj = ext.Get("diffuseTexture"); - if (diffObj.Has("index") && diffObj.Get("index").IsInt()) { - int texIndex = diffObj.Get("index").Get(); - if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) { - const auto &texture = gltfModel.textures[texIndex]; - int imageIndex = -1; - if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) { - imageIndex = texture.source; - } else { - auto extBasis = texture.extensions.find("KHR_texture_basisu"); - if (extBasis != texture.extensions.end()) { - const tinygltf::Value &e = extBasis->second; - if (e.Has("source") && e.Get("source").IsInt()) { - int src = e.Get("source").Get(); - if (src >= 0 && src < static_cast(gltfModel.images.size())) imageIndex = src; - } - } - } - if (imageIndex >= 0) { - const auto &image = gltfModel.images[imageIndex]; - std::string texIdOrPath; - if (!image.uri.empty()) { - texIdOrPath = baseTexturePath + image.uri; - // Schedule async load; libktx decoding will occur on renderer worker threads - renderer->LoadTextureAsync(texIdOrPath, true); - mat->albedoTexturePath = texIdOrPath; - std::cout << " Scheduled base color KTX2 file load (KHR_specGloss): " << texIdOrPath << std::endl; - } - if (mat->albedoTexturePath.empty() && !image.image.empty()) { - // Upload embedded image data (already decoded via our image loader when KTX2) - texIdOrPath = "gltf_baseColor_" + std::to_string(texIndex); - renderer->LoadTextureFromMemoryAsync(texIdOrPath, image.image.data(), image.width, image.height, image.component, true); - mat->albedoTexturePath = texIdOrPath; - std::cout << " Scheduled base color texture upload from memory (KHR_specGloss): " << texIdOrPath << std::endl; - } - } - } - } - } - } - } - - // Heuristic pass: fill missing baseColor (albedo) by deriving from normal map filenames - // Many Bistro materials have no baseColorTexture index. When that happens, try inferring - // the base color from the normal map by replacing common suffixes like _ddna -> _d/_c/_diffuse/_basecolor/_albedo. - for (auto& material : materials | std::views::values) { - Material* mat = material.get(); - if (!mat) continue; - if (!mat->albedoTexturePath.empty()) continue; // already set - // Only attempt if we have an external normal texture path to derive from - if (mat->normalTexturePath.empty()) continue; - const std::string &normalPath = mat->normalTexturePath; - // Skip embedded IDs like gltf_* which were already handled by memory uploads - if (normalPath.rfind("gltf_", 0) == 0) continue; - - std::string candidateBase = normalPath; - std::string normalLower = candidateBase; - for (auto &ch : normalLower) ch = static_cast(std::tolower(static_cast(ch))); - size_t pos = normalLower.find("_ddna"); - if (pos == std::string::npos) { - // Try a few additional normal suffixes seen in the wild - pos = normalLower.find("_n"); - } - if (pos != std::string::npos) { - static const char* suffixes[] = {"_d", "_c", "_cm", "_diffuse", "_basecolor", "_albedo"}; - for (const char* suf : suffixes) { - std::string cand = candidateBase; - cand.replace(pos, normalLower[pos]=='_' && normalLower.compare(pos, 5, "_ddna")==0 ? 5 : 2, suf); - // Ensure the file exists before attempting to load - if (std::filesystem::exists(cand)) { - // Schedule async load; libktx decoding will occur on renderer worker threads - renderer->LoadTextureAsync(cand, true); - mat->albedoTexturePath = cand; - std::cout << " Scheduled derived base color KTX2 load from normal sibling: " << cand << std::endl; - break; - } - } - } - } - - // Secondary heuristic: scan glTF images for base color by material-name match when still missing - for (auto & [materialName, materialPtr] : materials) { - Material* mat = materialPtr.get(); - if (!mat) continue; - if (!mat->albedoTexturePath.empty()) continue; // already resolved - // Try to find an image URI that looks like the base color for this material - std::string materialNameLower = materialName; - std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - for (const auto &image : gltfModel.images) { - if (image.uri.empty()) continue; - std::string imageUri = image.uri; - std::string imageUriLower = imageUri; - std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - bool looksBase = imageUriLower.find("basecolor") != std::string::npos || - imageUriLower.find("albedo") != std::string::npos || - imageUriLower.find("diffuse") != std::string::npos; - if (!looksBase) continue; - bool nameMatches = imageUriLower.find(materialNameLower) != std::string::npos; - if (!nameMatches) { - // Best-effort: try prefix of image name before '_' against material name - size_t underscore = imageUriLower.find('_'); - if (underscore != std::string::npos) { - std::string prefix = imageUriLower.substr(0, underscore); - nameMatches = materialNameLower.find(prefix) != std::string::npos; - } - } - if (!nameMatches) continue; - - std::string textureId = baseTexturePath + imageUri; // use path string as ID for cache - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - mat->albedoTexturePath = textureId; - std::cout << " Scheduled base color upload from memory (by name): " << textureId << std::endl; - break; - } else { - // Fallback: offload KTX2 file load to renderer threads - renderer->LoadTextureAsync(textureId); - mat->albedoTexturePath = textureId; - std::cout << " Scheduled base color KTX2 load from file (by name): " << textureId << std::endl; - break; - } - } - } - - // Process cameras from the GLTF file - if (!gltfModel.cameras.empty()) { - std::cout << "Found " << gltfModel.cameras.size() << " camera(s) in GLTF file" << std::endl; - - for (size_t i = 0; i < gltfModel.cameras.size(); ++i) { - const auto& gltfCamera = gltfModel.cameras[i]; - std::cout << " Camera " << i << ": " << gltfCamera.name << std::endl; - - // Store camera data in the model for later use - CameraData cameraData; - cameraData.name = gltfCamera.name.empty() ? ("camera_" + std::to_string(i)) : gltfCamera.name; - - if (gltfCamera.type == "perspective") { - cameraData.isPerspective = true; - cameraData.fov = static_cast(gltfCamera.perspective.yfov); - cameraData.aspectRatio = static_cast(gltfCamera.perspective.aspectRatio); - cameraData.nearPlane = static_cast(gltfCamera.perspective.znear); - cameraData.farPlane = static_cast(gltfCamera.perspective.zfar); - std::cout << " Perspective camera: FOV=" << cameraData.fov - << ", Aspect=" << cameraData.aspectRatio - << ", Near=" << cameraData.nearPlane - << ", Far=" << cameraData.farPlane << std::endl; - } else if (gltfCamera.type == "orthographic") { - cameraData.isPerspective = false; - cameraData.orthographicSize = static_cast(gltfCamera.orthographic.ymag); - cameraData.nearPlane = static_cast(gltfCamera.orthographic.znear); - cameraData.farPlane = static_cast(gltfCamera.orthographic.zfar); - std::cout << " Orthographic camera: Size=" << cameraData.orthographicSize - << ", Near=" << cameraData.nearPlane - << ", Far=" << cameraData.farPlane << std::endl; - } - - // Find the node that uses this camera to get transform information - for (const auto & node : gltfModel.nodes) { - if (node.camera == static_cast(i)) { - // Extract transform from node - if (node.translation.size() == 3) { - cameraData.position = glm::vec3( - static_cast(node.translation[0]), - static_cast(node.translation[1]), - static_cast(node.translation[2]) - ); - } - - if (node.rotation.size() == 4) { - cameraData.rotation = glm::quat( - static_cast(node.rotation[3]), // w - static_cast(node.rotation[0]), // x - static_cast(node.rotation[1]), // y - static_cast(node.rotation[2]) // z - ); - } - - std::cout << " Position: (" << cameraData.position.x << ", " - << cameraData.position.y << ", " << cameraData.position.z << ")" << std::endl; - break; - } - } - - model->cameras.push_back(cameraData); - } - } - - // Process scene hierarchy to get node transforms for meshes - std::map> meshInstanceTransforms; // Map from mesh index to all instance transforms - - // Helper function to calculate transform matrix from the GLTF node - auto calculateNodeTransform = [](const tinygltf::Node& node) -> glm::mat4 { - glm::mat4 transform; - - // Apply matrix if present - if (node.matrix.size() == 16) { - // GLTF matrices are column-major, the same as GLM - transform = glm::mat4( - node.matrix[0], node.matrix[1], node.matrix[2], node.matrix[3], - node.matrix[4], node.matrix[5], node.matrix[6], node.matrix[7], - node.matrix[8], node.matrix[9], node.matrix[10], node.matrix[11], - node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15] - ); - } else { - // Build transform from TRS components - glm::mat4 translation = glm::mat4(1.0f); - glm::mat4 rotation = glm::mat4(1.0f); - glm::mat4 scale = glm::mat4(1.0f); - - // Translation - if (node.translation.size() == 3) { - translation = glm::translate(glm::mat4(1.0f), glm::vec3( - static_cast(node.translation[0]), - static_cast(node.translation[1]), - static_cast(node.translation[2]) - )); - } - - // Rotation (quaternion) - if (node.rotation.size() == 4) { - glm::quat quat( - static_cast(node.rotation[3]), // w - static_cast(node.rotation[0]), // x - static_cast(node.rotation[1]), // y - static_cast(node.rotation[2]) // z - ); - rotation = glm::mat4_cast(quat); - } - - // Scale - if (node.scale.size() == 3) { - scale = glm::scale(glm::mat4(1.0f), glm::vec3( - static_cast(node.scale[0]), - static_cast(node.scale[1]), - static_cast(node.scale[2]) - )); - } - - // Combine: T * R * S - transform = translation * rotation * scale; - } - - return transform; - }; - - // Recursive function to traverse scene hierarchy - std::function traverseNode = [&](int nodeIndex, const glm::mat4& parentTransform) { - if (nodeIndex < 0 || nodeIndex >= gltfModel.nodes.size()) { - return; - } - - const tinygltf::Node& node = gltfModel.nodes[nodeIndex]; - - // Calculate this node's transform - glm::mat4 nodeTransform = calculateNodeTransform(node); - glm::mat4 worldTransform = parentTransform * nodeTransform; - - // If this node has a mesh, add the transform to the instances list - if (node.mesh >= 0 && node.mesh < gltfModel.meshes.size()) { - meshInstanceTransforms[node.mesh].push_back(worldTransform); - } - - // Recursively process children - for (int childIndex : node.children) { - traverseNode(childIndex, worldTransform); - } - }; - - // Process all scenes (typically there's only one default scene) - if (!gltfModel.scenes.empty()) { - int defaultScene = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; - if (defaultScene < gltfModel.scenes.size()) { - const tinygltf::Scene& scene = gltfModel.scenes[defaultScene]; - - // Traverse all root nodes in the scene - for (int rootNodeIndex : scene.nodes) { - traverseNode(rootNodeIndex, glm::mat4(1.0f)); - } - } - } - - std::map geometryMaterialMeshMap; // Map from geometry+material hash to unique MaterialMesh - - // Helper function to create a geometry hash for deduplication - auto createGeometryHash = [](const tinygltf::Primitive& primitive, int materialIndex) -> std::string { - std::string hash = "mat_" + std::to_string(materialIndex); - - // Add primitive attribute hashes to ensure unique geometry identification - if (primitive.indices >= 0) { - hash += "_idx_" + std::to_string(primitive.indices); - } - - for (const auto& [attrName, type] : primitive.attributes) { - hash += "_" + attrName + "_" + std::to_string(type); - } - - return hash; - }; - - // Process all meshes with improved instancing support - for (size_t meshIndex = 0; meshIndex < gltfModel.meshes.size(); ++meshIndex) { - const auto& mesh = gltfModel.meshes[meshIndex]; - - // Check if this mesh has instances - auto instanceIt = meshInstanceTransforms.find(static_cast(meshIndex)); - std::vector instances; - - if (instanceIt == meshInstanceTransforms.end() || instanceIt->second.empty()) { - instances.emplace_back(1.0f); // Identity transform at origin - } else { - instances = instanceIt->second; - } - - // Process each primitive (material group) in this mesh - for (const auto& primitive : mesh.primitives) { - // Get the material index for this primitive - int materialIndex = primitive.material; - if (materialIndex < 0) { - materialIndex = -1; // Use -1 for primitives without materials - } - - // Create a unique geometry hash for this primitive and material combination - std::string geometryHash = createGeometryHash(primitive, materialIndex); - - // Check if we already have this exact geometry and material combination - if (!geometryMaterialMeshMap.contains(geometryHash)) { - // Create a new MaterialMesh for this unique geometry and material combination - MaterialMesh materialMesh; - materialMesh.materialIndex = materialIndex; - - // Set material name - if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { - const auto& gltfMaterial = gltfModel.materials[materialIndex]; - materialMesh.materialName = gltfMaterial.name.empty() ? - ("material_" + std::to_string(materialIndex)) : gltfMaterial.name; - } else { - materialMesh.materialName = "no_material"; - } - - geometryMaterialMeshMap[geometryHash] = materialMesh; - } - - MaterialMesh& materialMesh = geometryMaterialMeshMap[geometryHash]; - - // Only process geometry if this MaterialMesh is empty (first time processing this geometry) -if (materialMesh.vertices.empty()) { - - auto vertexOffsetInMaterialMesh = static_cast(materialMesh.vertices.size()); - - // Get indices for this primitive (your existing code is correct) - if (primitive.indices >= 0) { - const tinygltf::Accessor& indexAccessor = gltfModel.accessors[primitive.indices]; - const tinygltf::BufferView& indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; - const tinygltf::Buffer& indexBuffer = gltfModel.buffers[indexBufferView.buffer]; - const void* indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; - if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - const auto* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); - } - } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - const auto* buf = static_cast(indexData); - for (size_t i = 0; i < indexAccessor.count; ++i) { - materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); - } - } - } - - // --- START: FINAL SAFE AND CORRECT VERTEX LOADING --- - - // Get the position accessor, which defines the vertex count. - auto posIt = primitive.attributes.find("POSITION"); - if (posIt == primitive.attributes.end()) continue; - const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; - - // Get data pointers and strides for all available attributes ONCE before the loop. - const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; - const tinygltf::Buffer& buffer = gltfModel.buffers[posBufferView.buffer]; - const unsigned char* pPositions = &buffer.data[posBufferView.byteOffset + posAccessor.byteOffset]; - const size_t posByteStride = posBufferView.byteStride == 0 ? sizeof(glm::vec3) : posBufferView.byteStride; - - const unsigned char* pNormals = nullptr; - size_t normalByteStride = 0; - auto normalIt = primitive.attributes.find("NORMAL"); - if (normalIt != primitive.attributes.end()) { - const tinygltf::Accessor& normalAccessor = gltfModel.accessors[normalIt->second]; - const tinygltf::BufferView& normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; - pNormals = &gltfModel.buffers[normalBufferView.buffer].data[normalBufferView.byteOffset + normalAccessor.byteOffset]; - normalByteStride = normalBufferView.byteStride == 0 ? sizeof(glm::vec3) : normalBufferView.byteStride; - } - - const unsigned char* pTexCoords = nullptr; - size_t texCoordByteStride = 0; - auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); - if (texCoordIt != primitive.attributes.end()) { - const tinygltf::Accessor& texCoordAccessor = gltfModel.accessors[texCoordIt->second]; - const tinygltf::BufferView& texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; - pTexCoords = &gltfModel.buffers[texCoordBufferView.buffer].data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]; - texCoordByteStride = texCoordBufferView.byteStride == 0 ? sizeof(glm::vec2) : texCoordBufferView.byteStride; - } - - const unsigned char* pTangents = nullptr; - size_t tangentByteStride = 0; - auto tangentIt = primitive.attributes.find("TANGENT"); - bool hasTangents = (tangentIt != primitive.attributes.end()); - if (hasTangents) { - const tinygltf::Accessor& tangentAccessor = gltfModel.accessors[tangentIt->second]; - const tinygltf::BufferView& tangentBufferView = gltfModel.bufferViews[tangentAccessor.bufferView]; - pTangents = &gltfModel.buffers[tangentBufferView.buffer].data[tangentBufferView.byteOffset + tangentAccessor.byteOffset]; - tangentByteStride = tangentBufferView.byteStride == 0 ? sizeof(glm::vec4) : tangentBufferView.byteStride; - } - - // Append vertices for this primitive preserving prior vertices - size_t baseVertex = materialMesh.vertices.size(); - materialMesh.vertices.resize(baseVertex + posAccessor.count); - - // Use a SINGLE, SAFE loop to load all vertex data. - for (size_t i = 0; i < posAccessor.count; ++i) { - auto& [position, normal, texCoord, tangent] = materialMesh.vertices[baseVertex + i]; - - position = *reinterpret_cast(pPositions + i * posByteStride); - - if (pNormals) { - normal = *reinterpret_cast(pNormals + i * normalByteStride); - } else { - normal = glm::vec3(0.0f, 0.0f, 1.0f); - } - // Normalize normals to ensure consistent magnitude - if (glm::dot(normal, normal) > 0.0f) { - normal = glm::normalize(normal); - } else { - normal = glm::vec3(0.0f, 0.0f, 1.0f); - } - - if (pTexCoords) { - texCoord = *reinterpret_cast(pTexCoords + i * texCoordByteStride); - } else { - texCoord = glm::vec2(0.0f, 0.0f); - } - - if (hasTangents && pTangents) { - // Load glTF tangent and ensure it is normalized and orthogonal to the normal. - glm::vec4 t4 = *reinterpret_cast(pTangents + i * tangentByteStride); - glm::vec3 T = glm::vec3(t4); - // Normalize tangent and make it orthogonal to normal to avoid skewed TBN - if (glm::dot(T, T) > 0.0f) { - T = glm::normalize(T); - T = glm::normalize(T - normal * glm::dot(normal, T)); - } else { - T = glm::vec3(1.0f, 0.0f, 0.0f); - } - float w = (t4.w >= 0.0f) ? 1.0f : -1.0f; // clamp handedness to +/-1 - tangent = glm::vec4(T, w); - } else { - // No tangents in source: use a safe default tangent (T=+X, handedness=+1) - tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - } - } - - // AFTER the mesh is fully built, generate tangents via MikkTSpace ONLY if the source mesh lacks glTF tangents. - if (!hasTangents) { - if (pNormals && pTexCoords && !materialMesh.indices.empty()) { - MikkTSpaceInterface mikkInterface; - mikkInterface.vertices = &materialMesh.vertices; - mikkInterface.indices = &materialMesh.indices; - - SMikkTSpaceInterface sm_interface{}; - sm_interface.m_getNumFaces = getNumFaces; - sm_interface.m_getNumVerticesOfFace = getNumVerticesOfFace; - sm_interface.m_getPosition = getPosition; - sm_interface.m_getNormal = getNormal; - sm_interface.m_getTexCoord = getTexCoord; - sm_interface.m_setTSpaceBasic = setTSpaceBasic; - - SMikkTSpaceContext mikk_context{}; - mikk_context.m_pInterface = &sm_interface; - mikk_context.m_pUserData = &mikkInterface; - - if (genTangSpaceDefault(&mikk_context)) { - std::cout << " Generated tangents (MikkTSpace) for material: " << materialMesh.materialName << std::endl; - } else { - std::cerr << " Failed to generate tangents for material: " << materialMesh.materialName << std::endl; - } - } else { - std::cout << " Skipping tangent generation (missing normals, UVs, or indices) for material: " << materialMesh.materialName << std::endl; - } - } else { - std::cout << " Using glTF-provided tangents for material: " << materialMesh.materialName << std::endl; - } - // --- END: FINAL SAFE AND CORRECT VERTEX LOADING --- +bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) +{ + std::cout << "Parsing GLTF file: " << filename << std::endl; + + // Extract the directory path from the model file to use as a base path for textures + std::filesystem::path modelPath(filename); + std::filesystem::path baseDir = std::filesystem::absolute(modelPath).parent_path(); + std::string baseTexturePath = baseDir.string(); + if (!baseTexturePath.empty() && baseTexturePath.back() != '/') + { + baseTexturePath += "/"; + } + std::cout << "Using base texture path: " << baseTexturePath << std::endl; + + // Create tinygltf loader + tinygltf::Model gltfModel; + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + // Set up image loader: prefer KTX2 via libktx; fallback to stb for other formats + loader.SetImageLoader([](tinygltf::Image *image, const int image_idx, std::string *err, + std::string *warn, int req_width, int req_height, + const unsigned char *bytes, int size, void *user_data) -> bool { + // Try KTX2 first using libktx + ktxTexture2 *ktxTex = nullptr; + KTX_error_code result = ktxTexture2_CreateFromMemory(bytes, size, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result == KTX_SUCCESS && ktxTex) + { + bool needsTranscode = ktxTexture2_NeedsTranscoding(ktxTex); + if (needsTranscode) + { + result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); + if (result != KTX_SUCCESS) + { + if (err) + *err = "Failed to transcode KTX2 image: " + std::to_string(result); + ktxTexture_Destroy((ktxTexture *) ktxTex); + return false; + } + } + image->width = static_cast(ktxTex->baseWidth); + image->height = static_cast(ktxTex->baseHeight); + image->component = 4; + image->bits = 8; + image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + + ktx_size_t offset; + ktxTexture_GetImageOffset((ktxTexture *) ktxTex, 0, 0, 0, &offset); + const uint8_t *levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; + size_t levelSize = needsTranscode ? static_cast(image->width) * static_cast(image->height) * 4 : ktxTexture_GetImageSize((ktxTexture *) ktxTex, 0); + image->image.resize(levelSize); + std::memcpy(image->image.data(), levelData, levelSize); + ktxTexture_Destroy((ktxTexture *) ktxTex); + return true; + } + + // Non-KTX images not supported by this loader per project simplification + if (err) + { + *err = "Non-KTX2 images are not supported by the custom image loader (use KTX2)."; + } + return false; + }, + nullptr); + + // Load the GLTF file + bool ret = false; + if (filename.find(".glb") != std::string::npos) + { + ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, filename); + } + else + { + ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, filename); + } + + if (!warn.empty()) + { + std::cout << "GLTF Warning: " << warn << std::endl; + } + + if (!err.empty()) + { + std::cerr << "GLTF Error: " << err << std::endl; + return false; + } + + if (!ret) + { + std::cerr << "Failed to parse GLTF file: " << filename << std::endl; + return false; + } + + // Extract mesh data from the first mesh (for now, we'll handle multiple meshes later) + if (gltfModel.meshes.empty()) + { + std::cerr << "No meshes found in GLTF file" << std::endl; + return false; + } + + light_scale = 1.0f; + // Test if generator is blender and apply the blender factor see the issue here: https://github.com/KhronosGroup/glTF/issues/2473 + if (gltfModel.asset.generator.find("blender") != std::string::npos) + { + std::cout << "Blender generator detected, applying blender factor" << std::endl; + light_scale = EMISSIVE_SCALE_FACTOR; + } + + // Track loaded textures to prevent loading the same texture multiple times + std::set loadedTextures; + + // Helper: lowercase a std::string (ASCII only) + auto toLower = [](const std::string &s) { + std::string out = s; + std::ranges::transform(out, out.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return out; + }; + + // Process materials first + for (size_t i = 0; i < gltfModel.materials.size(); ++i) + { + const auto &gltfMaterial = gltfModel.materials[i]; + + // Create PBR material + auto material = std::make_unique(gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name); + + // Extract PBR properties + if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 3) + { + material->albedo = glm::vec3( + gltfMaterial.pbrMetallicRoughness.baseColorFactor[0], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[1], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[2]); + if (gltfMaterial.pbrMetallicRoughness.baseColorFactor.size() >= 4) + { + material->alpha = static_cast(gltfMaterial.pbrMetallicRoughness.baseColorFactor[3]); + } + } + material->metallic = static_cast(gltfMaterial.pbrMetallicRoughness.metallicFactor); + material->roughness = static_cast(gltfMaterial.pbrMetallicRoughness.roughnessFactor); + + if (gltfMaterial.emissiveFactor.size() >= 3) + { + material->emissive = glm::vec3( + gltfMaterial.emissiveFactor[0], + gltfMaterial.emissiveFactor[1], + gltfMaterial.emissiveFactor[2]); + material->emissive *= light_scale; + } + + // Parse KHR_materials_emissive_strength extension + auto extensionIt = gltfMaterial.extensions.find("KHR_materials_emissive_strength"); + if (extensionIt != gltfMaterial.extensions.end()) + { + hasEmissiveStrengthExtension = true; + const tinygltf::Value &extension = extensionIt->second; + if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) + { + material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()); + } + } + else + { + material->emissiveStrength = 0.00058f; + } + + // Alpha mode / cutoff + material->alphaMode = gltfMaterial.alphaMode.empty() ? std::string("OPAQUE") : gltfMaterial.alphaMode; + material->alphaCutoff = static_cast(gltfMaterial.alphaCutoff); + + // Transmission (KHR_materials_transmission) + auto transIt = gltfMaterial.extensions.find("KHR_materials_transmission"); + if (transIt != gltfMaterial.extensions.end()) + { + const tinygltf::Value &ext = transIt->second; + if (ext.Has("transmissionFactor") && ext.Get("transmissionFactor").IsNumber()) + { + material->transmissionFactor = static_cast(ext.Get("transmissionFactor").Get()); + } + } + + // Classify obvious architectural glass and liquid materials for + // specialized rendering. This is a heuristic based primarily on + // material name. + { + std::string lowerName = toLower(material->GetName()); + bool nameSuggestsGlass = + (lowerName.find("glass") != std::string::npos) || + (lowerName.find("window") != std::string::npos); + + bool probablyLiquid = + (lowerName.find("beer") != std::string::npos) || + (lowerName.find("wine") != std::string::npos) || + (lowerName.find("liquid") != std::string::npos); + + if (nameSuggestsGlass && !probablyLiquid) + { + material->isGlass = true; + } + + if (probablyLiquid) + { + material->isLiquid = true; + + // Slightly boost liquid visibility. + material->albedo *= 1.4f; + material->albedo = glm::clamp(material->albedo, glm::vec3(0.0f), glm::vec3(4.0f)); + + // Slightly reduce roughness so specular highlights from + // lights help liquids stand out. + material->roughness = glm::clamp(material->roughness * 0.8f, 0.0f, 1.0f); + + // Ensure the liquid is not fully transparent by default. + material->alpha = glm::clamp(material->alpha * 1.2f, 0.15f, 1.0f); + } + } + + // Specular-Glossiness (KHR_materials_pbrSpecularGlossiness) + auto sgIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); + if (sgIt != gltfMaterial.extensions.end()) + { + const tinygltf::Value &ext = sgIt->second; + material->useSpecularGlossiness = true; + // diffuseFactor -> albedo and alpha + if (ext.Has("diffuseFactor") && ext.Get("diffuseFactor").IsArray()) + { + const auto &arr = ext.Get("diffuseFactor").Get(); + if (arr.size() >= 3) + { + material->albedo = glm::vec3( + arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->albedo.r, + arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->albedo.g, + arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->albedo.b); + if (arr.size() >= 4 && arr[3].IsNumber()) + { + material->alpha = static_cast(arr[3].Get()); + } + } + } + // specularFactor (vec3) + if (ext.Has("specularFactor") && ext.Get("specularFactor").IsArray()) + { + const auto &arr = ext.Get("specularFactor").Get(); + if (arr.size() >= 3) + { + material->specularFactor = glm::vec3( + arr[0].IsNumber() ? static_cast(arr[0].Get()) : material->specularFactor.r, + arr[1].IsNumber() ? static_cast(arr[1].Get()) : material->specularFactor.g, + arr[2].IsNumber() ? static_cast(arr[2].Get()) : material->specularFactor.b); + } + } + // glossinessFactor (float) + if (ext.Has("glossinessFactor") && ext.Get("glossinessFactor").IsNumber()) + { + material->glossinessFactor = static_cast(ext.Get("glossinessFactor").Get()); + } + + // Load diffuseTexture into albedoTexturePath if present + if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) + { + const auto &diffObj = ext.Get("diffuseTexture"); + if (diffObj.Has("index") && diffObj.Get("index").IsInt()) + { + int texIndex = diffObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) + { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) + { + imageIndex = texture.source; + } + else + { + auto extBasis = texture.extensions.find("KHR_texture_basisu"); + if (extBasis != texture.extensions.end()) + { + const tinygltf::Value &e = extBasis->second; + if (e.Has("source") && e.Get("source").IsInt()) + { + int src = e.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) + imageIndex = src; + } + } + } + if (imageIndex >= 0) + { + const auto &image = gltfModel.images[imageIndex]; + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->albedoTexturePath = textureId; + } + else if (!image.uri.empty()) + { + std::string filePath = baseTexturePath + image.uri; + renderer->LoadTextureAsync(filePath); + material->albedoTexturePath = filePath; + } + } + } + } + } + // Load specularGlossinessTexture into specGlossTexturePath and mirror to metallicRoughnessTexturePath (binding 2) + if (ext.Has("specularGlossinessTexture") && ext.Get("specularGlossinessTexture").IsObject()) + { + const auto &sgObj = ext.Get("specularGlossinessTexture"); + if (sgObj.Has("index") && sgObj.Get("index").IsInt()) + { + int texIndex = sgObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) + { + std::string textureId = "gltf_specGloss_" + std::to_string(texIndex); + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Embedded image data (already decoded by tinygltf image loader) + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, false); + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 + } + else if (!image.uri.empty()) + { + // External KTX2 file: offload libktx decode + upload to renderer worker threads + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 + } + } + } + } + } + } + + // Extract texture information and load embedded texture data + if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) + { + int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + imageIndex = texture.source; + } + else + { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) + { + const tinygltf::Value &ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) + { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) + { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) + { + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); + material->albedoTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[imageIndex]; + std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; + if (!image.image.empty()) + { + // Always use memory-based upload (KTX2 already decoded by SetImageLoader) + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); + material->albedoTexturePath = textureId; + std::cout << " Scheduled base color texture upload from memory: " << textureId << std::endl; + } + else if (!image.uri.empty()) + { + // Offload KTX2 file reading/upload to renderer thread pool + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath, true); + material->albedoTexturePath = textureId; + std::cout << " Scheduled base color KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded image bytes for base color texture index " << texIndex << std::endl; + } + } + } + } + + if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) + { + int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->metallicRoughnessTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Load embedded texture data asynchronously + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded metallic-roughness texture upload: " << textureId << std::endl; + } + else if (!image.uri.empty()) + { + // Offload KTX2 file reading/upload to renderer thread pool + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->metallicRoughnessTexturePath = textureId; + std::cout << " Scheduled metallic-roughness KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; + } + } + } + } + + if (gltfMaterial.normalTexture.index >= 0) + { + int texIndex = gltfMaterial.normalTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + imageIndex = texture.source; + } + else + { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) + { + const tinygltf::Value &ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) + { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) + { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->normalTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[imageIndex]; + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->normalTexturePath = textureId; + std::cout << " Scheduled normal texture upload from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else if (!image.uri.empty()) + { + // Offload KTX2 file reading/upload to renderer thread pool + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->normalTexturePath = textureId; + std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; + } + } + } + } + + if (gltfMaterial.occlusionTexture.index >= 0) + { + int texIndex = gltfMaterial.occlusionTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->occlusionTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Schedule embedded texture upload + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded occlusion texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else if (!image.uri.empty()) + { + // Offload KTX2 file reading/upload to renderer thread pool + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->occlusionTexturePath = textureId; + std::cout << " Scheduled occlusion KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; + } + } + } + } + + if (gltfMaterial.emissiveTexture.index >= 0) + { + int texIndex = gltfMaterial.emissiveTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + material->emissiveTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Schedule embedded texture upload + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded emissive texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else if (!image.uri.empty()) + { + // Offload KTX2 file reading/upload to renderer thread pool + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + material->emissiveTexturePath = textureId; + std::cout << " Scheduled emissive KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for emissive texture index " << texIndex << std::endl; + } + } + } + } + + // Store the material + materials[material->GetName()] = std::move(material); + } + + // Handle KHR_materials_pbrSpecularGlossiness.diffuseTexture for baseColor when still missing + for (size_t i = 0; i < gltfModel.materials.size(); ++i) + { + const auto &gltfMaterial = gltfModel.materials[i]; + std::string matName = gltfMaterial.name.empty() ? ("material_" + std::to_string(i)) : gltfMaterial.name; + auto matIt = materials.find(matName); + if (matIt == materials.end()) + continue; + Material *mat = matIt->second.get(); + if (!mat || !mat->albedoTexturePath.empty()) + continue; + auto extIt = gltfMaterial.extensions.find("KHR_materials_pbrSpecularGlossiness"); + if (extIt != gltfMaterial.extensions.end()) + { + const tinygltf::Value &ext = extIt->second; + if (ext.Has("diffuseTexture") && ext.Get("diffuseTexture").IsObject()) + { + const auto &diffObj = ext.Get("diffuseTexture"); + if (diffObj.Has("index") && diffObj.Get("index").IsInt()) + { + int texIndex = diffObj.Get("index").Get(); + if (texIndex >= 0 && texIndex < static_cast(gltfModel.textures.size())) + { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < static_cast(gltfModel.images.size())) + { + imageIndex = texture.source; + } + else + { + auto extBasis = texture.extensions.find("KHR_texture_basisu"); + if (extBasis != texture.extensions.end()) + { + const tinygltf::Value &e = extBasis->second; + if (e.Has("source") && e.Get("source").IsInt()) + { + int src = e.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) + imageIndex = src; + } + } + } + if (imageIndex >= 0) + { + const auto &image = gltfModel.images[imageIndex]; + std::string texIdOrPath; + if (!image.uri.empty()) + { + texIdOrPath = baseTexturePath + image.uri; + // Schedule async load; libktx decoding will occur on renderer worker threads + renderer->LoadTextureAsync(texIdOrPath, true); + mat->albedoTexturePath = texIdOrPath; + std::cout << " Scheduled base color KTX2 file load (KHR_specGloss): " << texIdOrPath << std::endl; + } + if (mat->albedoTexturePath.empty() && !image.image.empty()) + { + // Upload embedded image data (already decoded via our image loader when KTX2) + texIdOrPath = "gltf_baseColor_" + std::to_string(texIndex); + renderer->LoadTextureFromMemoryAsync(texIdOrPath, image.image.data(), image.width, image.height, image.component, true); + mat->albedoTexturePath = texIdOrPath; + std::cout << " Scheduled base color texture upload from memory (KHR_specGloss): " << texIdOrPath << std::endl; + } + } + } + } + } + } + } + + // Heuristic pass: fill missing baseColor (albedo) by deriving from normal map filenames + // Many Bistro materials have no baseColorTexture index. When that happens, try inferring + // the base color from the normal map by replacing common suffixes like _ddna -> _d/_c/_diffuse/_basecolor/_albedo. + for (auto &kv : materials) + { + auto &material = kv.second; + Material *mat = material.get(); + if (!mat) + continue; + if (!mat->albedoTexturePath.empty()) + continue; // already set + // Only attempt if we have an external normal texture path to derive from + if (mat->normalTexturePath.empty()) + continue; + const std::string &normalPath = mat->normalTexturePath; + // Skip embedded IDs like gltf_* which were already handled by memory uploads + if (normalPath.rfind("gltf_", 0) == 0) + continue; + + std::string candidateBase = normalPath; + std::string normalLower = candidateBase; + for (auto &ch : normalLower) + ch = static_cast(std::tolower(static_cast(ch))); + size_t pos = normalLower.find("_ddna"); + if (pos == std::string::npos) + { + // Try a few additional normal suffixes seen in the wild + pos = normalLower.find("_n"); + } + if (pos != std::string::npos) + { + static const char *suffixes[] = {"_d", "_c", "_cm", "_diffuse", "_basecolor", "_albedo"}; + for (const char *suf : suffixes) + { + std::string cand = candidateBase; + cand.replace(pos, normalLower[pos] == '_' && normalLower.compare(pos, 5, "_ddna") == 0 ? 5 : 2, suf); + // Ensure the file exists before attempting to load + if (std::filesystem::exists(cand)) + { + // Schedule async load; libktx decoding will occur on renderer worker threads + renderer->LoadTextureAsync(cand, true); + mat->albedoTexturePath = cand; + std::cout << " Scheduled derived base color KTX2 load from normal sibling: " << cand << std::endl; + break; + } + } + } + } + + // Secondary heuristic: scan glTF images for base color by material-name match when still missing + for (auto &[materialName, materialPtr] : materials) + { + Material *mat = materialPtr.get(); + if (!mat) + continue; + if (!mat->albedoTexturePath.empty()) + continue; // already resolved + // Try to find an image URI that looks like the base color for this material + std::string materialNameLower = materialName; + std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + for (const auto &image : gltfModel.images) + { + if (image.uri.empty()) + continue; + std::string imageUri = image.uri; + std::string imageUriLower = imageUri; + std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + bool looksBase = imageUriLower.find("basecolor") != std::string::npos || + imageUriLower.find("albedo") != std::string::npos || + imageUriLower.find("diffuse") != std::string::npos; + if (!looksBase) + continue; + bool nameMatches = imageUriLower.find(materialNameLower) != std::string::npos; + if (!nameMatches) + { + // Best-effort: try prefix of image name before '_' against material name + size_t underscore = imageUriLower.find('_'); + if (underscore != std::string::npos) + { + std::string prefix = imageUriLower.substr(0, underscore); + nameMatches = materialNameLower.find(prefix) != std::string::npos; + } + } + if (!nameMatches) + continue; + + std::string textureId = baseTexturePath + imageUri; // use path string as ID for cache + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + mat->albedoTexturePath = textureId; + std::cout << " Scheduled base color upload from memory (by name): " << textureId << std::endl; + break; + } + else + { + // Fallback: offload KTX2 file load to renderer threads + renderer->LoadTextureAsync(textureId); + mat->albedoTexturePath = textureId; + std::cout << " Scheduled base color KTX2 load from file (by name): " << textureId << std::endl; + break; + } + } + } + + // Process cameras from the GLTF file + if (!gltfModel.cameras.empty()) + { + std::cout << "Found " << gltfModel.cameras.size() << " camera(s) in GLTF file" << std::endl; + + for (size_t i = 0; i < gltfModel.cameras.size(); ++i) + { + const auto &gltfCamera = gltfModel.cameras[i]; + std::cout << " Camera " << i << ": " << gltfCamera.name << std::endl; + + // Store camera data in the model for later use + CameraData cameraData; + cameraData.name = gltfCamera.name.empty() ? ("camera_" + std::to_string(i)) : gltfCamera.name; + + if (gltfCamera.type == "perspective") + { + cameraData.isPerspective = true; + cameraData.fov = static_cast(gltfCamera.perspective.yfov); + cameraData.aspectRatio = static_cast(gltfCamera.perspective.aspectRatio); + cameraData.nearPlane = static_cast(gltfCamera.perspective.znear); + cameraData.farPlane = static_cast(gltfCamera.perspective.zfar); + std::cout << " Perspective camera: FOV=" << cameraData.fov + << ", Aspect=" << cameraData.aspectRatio + << ", Near=" << cameraData.nearPlane + << ", Far=" << cameraData.farPlane << std::endl; + } + else if (gltfCamera.type == "orthographic") + { + cameraData.isPerspective = false; + cameraData.orthographicSize = static_cast(gltfCamera.orthographic.ymag); + cameraData.nearPlane = static_cast(gltfCamera.orthographic.znear); + cameraData.farPlane = static_cast(gltfCamera.orthographic.zfar); + std::cout << " Orthographic camera: Size=" << cameraData.orthographicSize + << ", Near=" << cameraData.nearPlane + << ", Far=" << cameraData.farPlane << std::endl; + } + + // Find the node that uses this camera to get transform information + for (const auto &node : gltfModel.nodes) + { + if (node.camera == static_cast(i)) + { + // Extract transform from node + if (node.translation.size() == 3) + { + cameraData.position = glm::vec3( + static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2])); + } + + if (node.rotation.size() == 4) + { + cameraData.rotation = glm::quat( + static_cast(node.rotation[3]), // w + static_cast(node.rotation[0]), // x + static_cast(node.rotation[1]), // y + static_cast(node.rotation[2]) // z + ); + } + + std::cout << " Position: (" << cameraData.position.x << ", " + << cameraData.position.y << ", " << cameraData.position.z << ")" << std::endl; + break; + } + } + + model->cameras.push_back(cameraData); + } + } + + // Process animations from the GLTF file + if (!gltfModel.animations.empty()) + { + std::cout << "Found " << gltfModel.animations.size() << " animation(s) in GLTF file" << std::endl; + + std::vector parsedAnimations; + parsedAnimations.reserve(gltfModel.animations.size()); + + for (size_t animIdx = 0; animIdx < gltfModel.animations.size(); ++animIdx) + { + const auto &gltfAnim = gltfModel.animations[animIdx]; + + Animation anim; + anim.name = gltfAnim.name.empty() ? ("animation_" + std::to_string(animIdx)) : gltfAnim.name; + + // Parse samplers + anim.samplers.reserve(gltfAnim.samplers.size()); + for (const auto &gltfSampler : gltfAnim.samplers) + { + AnimationSampler sampler; + + // Parse interpolation type + if (gltfSampler.interpolation == "STEP") + { + sampler.interpolation = AnimationInterpolation::Step; + } + else if (gltfSampler.interpolation == "CUBICSPLINE") + { + sampler.interpolation = AnimationInterpolation::CubicSpline; + } + else + { + sampler.interpolation = AnimationInterpolation::Linear; + } + + // Read input (time) accessor + if (gltfSampler.input >= 0 && gltfSampler.input < static_cast(gltfModel.accessors.size())) + { + const auto &inputAccessor = gltfModel.accessors[gltfSampler.input]; + const auto &inputBufferView = gltfModel.bufferViews[inputAccessor.bufferView]; + const auto &inputBuffer = gltfModel.buffers[inputBufferView.buffer]; + + const float *inputData = reinterpret_cast( + &inputBuffer.data[inputBufferView.byteOffset + inputAccessor.byteOffset]); + + sampler.inputTimes.resize(inputAccessor.count); + for (size_t i = 0; i < inputAccessor.count; ++i) + { + sampler.inputTimes[i] = inputData[i]; + } + } + + // Read output (value) accessor + if (gltfSampler.output >= 0 && gltfSampler.output < static_cast(gltfModel.accessors.size())) + { + const auto &outputAccessor = gltfModel.accessors[gltfSampler.output]; + const auto &outputBufferView = gltfModel.bufferViews[outputAccessor.bufferView]; + const auto &outputBuffer = gltfModel.buffers[outputBufferView.buffer]; + + const float *outputData = reinterpret_cast( + &outputBuffer.data[outputBufferView.byteOffset + outputAccessor.byteOffset]); + + // Determine number of floats per element based on accessor type + size_t componentsPerElement = 1; + if (outputAccessor.type == TINYGLTF_TYPE_VEC3) + { + componentsPerElement = 3; + } + else if (outputAccessor.type == TINYGLTF_TYPE_VEC4) + { + componentsPerElement = 4; + } + + size_t totalFloats = outputAccessor.count * componentsPerElement; + sampler.outputValues.resize(totalFloats); + for (size_t i = 0; i < totalFloats; ++i) + { + sampler.outputValues[i] = outputData[i]; + } + } + + anim.samplers.push_back(std::move(sampler)); + } + + // Parse channels + anim.channels.reserve(gltfAnim.channels.size()); + for (const auto &gltfChannel : gltfAnim.channels) + { + AnimationChannel channel; + channel.samplerIndex = gltfChannel.sampler; + channel.targetNode = gltfChannel.target_node; + + // Parse target path + if (gltfChannel.target_path == "translation") + { + channel.path = AnimationPath::Translation; + } + else if (gltfChannel.target_path == "rotation") + { + channel.path = AnimationPath::Rotation; + } + else if (gltfChannel.target_path == "scale") + { + channel.path = AnimationPath::Scale; + } + else if (gltfChannel.target_path == "weights") + { + channel.path = AnimationPath::Weights; + } + + anim.channels.push_back(channel); + } + + std::cout << " Animation '" << anim.name << "': " + << anim.samplers.size() << " samplers, " + << anim.channels.size() << " channels, " + << "duration=" << anim.GetDuration() << "s" << std::endl; + + parsedAnimations.push_back(std::move(anim)); + } + + model->SetAnimations(parsedAnimations); + std::cout << "Loaded " << parsedAnimations.size() << " animations into model" << std::endl; + } + + // Collect all animated node indices from parsed animations + std::set animatedNodeIndices; + for (const auto &anim : model->GetAnimations()) + { + for (const auto &channel : anim.channels) + { + if (channel.targetNode >= 0) + { + animatedNodeIndices.insert(channel.targetNode); + } + } + } + if (!animatedNodeIndices.empty()) + { + std::cout << "[Animation] Found " << animatedNodeIndices.size() << " unique animated node(s)" << std::endl; + } + + // Process scene hierarchy to get node transforms for meshes + std::map> meshInstanceTransforms; // Map from mesh index to all instance transforms + std::unordered_map animatedNodeTransforms; // Map from animated node index to world transform + std::unordered_map animatedNodeMeshes; // Map from animated node index to mesh index + + // Helper function to calculate transform matrix from the GLTF node + auto calculateNodeTransform = [](const tinygltf::Node &node) -> glm::mat4 { + glm::mat4 transform; + + // Apply matrix if present + if (node.matrix.size() == 16) + { + // GLTF matrices are column-major, the same as GLM + transform = glm::mat4( + node.matrix[0], node.matrix[1], node.matrix[2], node.matrix[3], + node.matrix[4], node.matrix[5], node.matrix[6], node.matrix[7], + node.matrix[8], node.matrix[9], node.matrix[10], node.matrix[11], + node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15]); + } + else + { + // Build transform from TRS components + glm::mat4 translation = glm::mat4(1.0f); + glm::mat4 rotation = glm::mat4(1.0f); + glm::mat4 scale = glm::mat4(1.0f); + + // Translation + if (node.translation.size() == 3) + { + translation = glm::translate(glm::mat4(1.0f), glm::vec3( + static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2]))); + } + + // Rotation (quaternion) + if (node.rotation.size() == 4) + { + glm::quat quat( + static_cast(node.rotation[3]), // w + static_cast(node.rotation[0]), // x + static_cast(node.rotation[1]), // y + static_cast(node.rotation[2]) // z + ); + rotation = glm::mat4_cast(quat); + } + + // Scale + if (node.scale.size() == 3) + { + scale = glm::scale(glm::mat4(1.0f), glm::vec3( + static_cast(node.scale[0]), + static_cast(node.scale[1]), + static_cast(node.scale[2]))); + } + + // Combine: T * R * S + transform = translation * rotation * scale; + } + + return transform; + }; + + // Recursive function to traverse scene hierarchy + std::function traverseNode = [&](int nodeIndex, const glm::mat4 &parentTransform) { + if (nodeIndex < 0 || nodeIndex >= gltfModel.nodes.size()) + { + return; + } + + const tinygltf::Node &node = gltfModel.nodes[nodeIndex]; + + // Calculate this node's transform + glm::mat4 nodeTransform = calculateNodeTransform(node); + glm::mat4 worldTransform = parentTransform * nodeTransform; + + // If this node has a mesh, add the transform to the instances list + if (node.mesh >= 0 && node.mesh < gltfModel.meshes.size()) + { + meshInstanceTransforms[node.mesh].push_back(worldTransform); + } + + // If this node is animated, capture its world transform and mesh reference + if (animatedNodeIndices.contains(nodeIndex)) + { + animatedNodeTransforms[nodeIndex] = worldTransform; + if (node.mesh >= 0) + { + animatedNodeMeshes[nodeIndex] = node.mesh; + std::cout << "[Animation] Captured transform for animated node " << nodeIndex + << " (" << node.name << ") with mesh " << node.mesh << std::endl; + } + else + { + std::cout << "[Animation] Captured transform for animated node " << nodeIndex + << " (" << node.name << ") - no mesh" << std::endl; + } + } + + // Recursively process children + for (int childIndex : node.children) + { + traverseNode(childIndex, worldTransform); + } + }; + + // Process all scenes (typically there's only one default scene) + if (!gltfModel.scenes.empty()) + { + int defaultScene = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; + if (defaultScene < gltfModel.scenes.size()) + { + const tinygltf::Scene &scene = gltfModel.scenes[defaultScene]; + + // Traverse all root nodes in the scene + for (int rootNodeIndex : scene.nodes) + { + traverseNode(rootNodeIndex, glm::mat4(1.0f)); + } + } + } + + // Store animated node transforms in the model for use by AnimationComponent + if (!animatedNodeTransforms.empty()) + { + model->SetAnimatedNodeTransforms(animatedNodeTransforms); + std::cout << "[Animation] Stored " << animatedNodeTransforms.size() + << " animated node transform(s) in model" << std::endl; + } + + // Store animated node mesh mappings for linking geometry entities to animations + if (!animatedNodeMeshes.empty()) + { + model->SetAnimatedNodeMeshes(animatedNodeMeshes); + std::cout << "[Animation] Stored " << animatedNodeMeshes.size() + << " animated node mesh mapping(s) in model" << std::endl; + } + + std::map geometryMaterialMeshMap; // Map from geometry+material hash to unique MaterialMesh + + // Helper function to create a geometry hash for deduplication + auto createGeometryHash = [](const tinygltf::Primitive &primitive, int materialIndex) -> std::string { + std::string hash = "mat_" + std::to_string(materialIndex); + + // Add primitive attribute hashes to ensure unique geometry identification + if (primitive.indices >= 0) + { + hash += "_idx_" + std::to_string(primitive.indices); + } + + for (const auto &[attrName, type] : primitive.attributes) + { + hash += "_" + attrName + "_" + std::to_string(type); + } + + return hash; + }; + + // Process all meshes with improved instancing support + for (size_t meshIndex = 0; meshIndex < gltfModel.meshes.size(); ++meshIndex) + { + const auto &mesh = gltfModel.meshes[meshIndex]; + + // Check if this mesh has instances + auto instanceIt = meshInstanceTransforms.find(static_cast(meshIndex)); + std::vector instances; + + if (instanceIt == meshInstanceTransforms.end() || instanceIt->second.empty()) + { + instances.emplace_back(1.0f); // Identity transform at origin + } + else + { + instances = instanceIt->second; + } + + // Process each primitive (material group) in this mesh + for (const auto &primitive : mesh.primitives) + { + // Get the material index for this primitive + int materialIndex = primitive.material; + if (materialIndex < 0) + { + materialIndex = -1; // Use -1 for primitives without materials + } + + // Create a unique geometry hash for this primitive and material combination + std::string geometryHash = createGeometryHash(primitive, materialIndex); + + // Check if we already have this exact geometry and material combination + if (!geometryMaterialMeshMap.contains(geometryHash)) + { + // Create a new MaterialMesh for this unique geometry and material combination + MaterialMesh materialMesh; + materialMesh.materialIndex = materialIndex; + materialMesh.sourceMeshIndex = static_cast(meshIndex); // Track source mesh for animations + + // Set material name + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) + { + const auto &gltfMaterial = gltfModel.materials[materialIndex]; + materialMesh.materialName = gltfMaterial.name.empty() ? + ("material_" + std::to_string(materialIndex)) : + gltfMaterial.name; + } + else + { + materialMesh.materialName = "no_material"; + } + + geometryMaterialMeshMap[geometryHash] = materialMesh; + } + + MaterialMesh &materialMesh = geometryMaterialMeshMap[geometryHash]; + + // Only process geometry if this MaterialMesh is empty (first time processing this geometry) + if (materialMesh.vertices.empty()) + { + auto vertexOffsetInMaterialMesh = static_cast(materialMesh.vertices.size()); + + // Get indices for this primitive (your existing code is correct) + if (primitive.indices >= 0) + { + const tinygltf::Accessor &indexAccessor = gltfModel.accessors[primitive.indices]; + const tinygltf::BufferView &indexBufferView = gltfModel.bufferViews[indexAccessor.bufferView]; + const tinygltf::Buffer &indexBuffer = gltfModel.buffers[indexBufferView.buffer]; + const void *indexData = &indexBuffer.data[indexBufferView.byteOffset + indexAccessor.byteOffset]; + if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) + { + const auto *buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) + { + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); + } + } + else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) + { + const auto *buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) + { + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); + } + } + } + + // --- START: FINAL SAFE AND CORRECT VERTEX LOADING --- + + // Get the position accessor, which defines the vertex count. + auto posIt = primitive.attributes.find("POSITION"); + if (posIt == primitive.attributes.end()) + continue; + const tinygltf::Accessor &posAccessor = gltfModel.accessors[posIt->second]; + + // Get data pointers and strides for all available attributes ONCE before the loop. + const tinygltf::BufferView &posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; + const tinygltf::Buffer &buffer = gltfModel.buffers[posBufferView.buffer]; + const unsigned char *pPositions = &buffer.data[posBufferView.byteOffset + posAccessor.byteOffset]; + const size_t posByteStride = posBufferView.byteStride == 0 ? sizeof(glm::vec3) : posBufferView.byteStride; + + const unsigned char *pNormals = nullptr; + size_t normalByteStride = 0; + auto normalIt = primitive.attributes.find("NORMAL"); + if (normalIt != primitive.attributes.end()) + { + const tinygltf::Accessor &normalAccessor = gltfModel.accessors[normalIt->second]; + const tinygltf::BufferView &normalBufferView = gltfModel.bufferViews[normalAccessor.bufferView]; + pNormals = &gltfModel.buffers[normalBufferView.buffer].data[normalBufferView.byteOffset + normalAccessor.byteOffset]; + normalByteStride = normalBufferView.byteStride == 0 ? sizeof(glm::vec3) : normalBufferView.byteStride; + } + + const unsigned char *pTexCoords = nullptr; + size_t texCoordByteStride = 0; + auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); + if (texCoordIt != primitive.attributes.end()) + { + const tinygltf::Accessor &texCoordAccessor = gltfModel.accessors[texCoordIt->second]; + const tinygltf::BufferView &texCoordBufferView = gltfModel.bufferViews[texCoordAccessor.bufferView]; + pTexCoords = &gltfModel.buffers[texCoordBufferView.buffer].data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]; + texCoordByteStride = texCoordBufferView.byteStride == 0 ? sizeof(glm::vec2) : texCoordBufferView.byteStride; + } + + const unsigned char *pTangents = nullptr; + size_t tangentByteStride = 0; + auto tangentIt = primitive.attributes.find("TANGENT"); + bool hasTangents = (tangentIt != primitive.attributes.end()); + if (hasTangents) + { + const tinygltf::Accessor &tangentAccessor = gltfModel.accessors[tangentIt->second]; + const tinygltf::BufferView &tangentBufferView = gltfModel.bufferViews[tangentAccessor.bufferView]; + pTangents = &gltfModel.buffers[tangentBufferView.buffer].data[tangentBufferView.byteOffset + tangentAccessor.byteOffset]; + tangentByteStride = tangentBufferView.byteStride == 0 ? sizeof(glm::vec4) : tangentBufferView.byteStride; + } + + // Append vertices for this primitive preserving prior vertices + size_t baseVertex = materialMesh.vertices.size(); + materialMesh.vertices.resize(baseVertex + posAccessor.count); + + // Use a SINGLE, SAFE loop to load all vertex data. + for (size_t i = 0; i < posAccessor.count; ++i) + { + auto &[position, normal, texCoord, tangent] = materialMesh.vertices[baseVertex + i]; + + position = *reinterpret_cast(pPositions + i * posByteStride); + + if (pNormals) + { + normal = *reinterpret_cast(pNormals + i * normalByteStride); + } + else + { + normal = glm::vec3(0.0f, 0.0f, 1.0f); + } + // Normalize normals to ensure consistent magnitude + if (glm::dot(normal, normal) > 0.0f) + { + normal = glm::normalize(normal); + } + else + { + normal = glm::vec3(0.0f, 0.0f, 1.0f); + } + + if (pTexCoords) + { + texCoord = *reinterpret_cast(pTexCoords + i * texCoordByteStride); + } + else + { + texCoord = glm::vec2(0.0f, 0.0f); + } + + if (hasTangents && pTangents) + { + // Load glTF tangent and ensure it is normalized and orthogonal to the normal. + glm::vec4 t4 = *reinterpret_cast(pTangents + i * tangentByteStride); + glm::vec3 T = glm::vec3(t4); + // Normalize tangent and make it orthogonal to normal to avoid skewed TBN + if (glm::dot(T, T) > 0.0f) + { + T = glm::normalize(T); + T = glm::normalize(T - normal * glm::dot(normal, T)); + } + else + { + T = glm::vec3(1.0f, 0.0f, 0.0f); + } + float w = (t4.w >= 0.0f) ? 1.0f : -1.0f; // clamp handedness to +/-1 + tangent = glm::vec4(T, w); + } + else + { + // No tangents in source: use a safe default tangent (T=+X, handedness=+1) + tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + } + } + + // AFTER the mesh is fully built, generate tangents via MikkTSpace ONLY if the source mesh lacks glTF tangents. + if (!hasTangents) + { + if (pNormals && pTexCoords && !materialMesh.indices.empty()) + { + MikkTSpaceInterface mikkInterface; + mikkInterface.vertices = &materialMesh.vertices; + mikkInterface.indices = &materialMesh.indices; + + SMikkTSpaceInterface sm_interface{}; + sm_interface.m_getNumFaces = getNumFaces; + sm_interface.m_getNumVerticesOfFace = getNumVerticesOfFace; + sm_interface.m_getPosition = getPosition; + sm_interface.m_getNormal = getNormal; + sm_interface.m_getTexCoord = getTexCoord; + sm_interface.m_setTSpaceBasic = setTSpaceBasic; + + SMikkTSpaceContext mikk_context{}; + mikk_context.m_pInterface = &sm_interface; + mikk_context.m_pUserData = &mikkInterface; + + if (genTangSpaceDefault(&mikk_context)) + { + std::cout << " Generated tangents (MikkTSpace) for material: " << materialMesh.materialName << std::endl; + } + else + { + std::cerr << " Failed to generate tangents for material: " << materialMesh.materialName << std::endl; + } + } + else + { + std::cout << " Skipping tangent generation (missing normals, UVs, or indices) for material: " << materialMesh.materialName << std::endl; + } + } + else + { + std::cout << " Using glTF-provided tangents for material: " << materialMesh.materialName << std::endl; + } + // --- END: FINAL SAFE AND CORRECT VERTEX LOADING --- + } + + // Add all instances to this MaterialMesh (both new and existing geometry) + for (const glm::mat4 &instanceTransform : instances) + { + materialMesh.AddInstance(instanceTransform, static_cast(materialIndex)); + } + } + } + + // Convert geometry-based material mesh map to vector + std::vector modelMaterialMeshes; + for (auto &kv : geometryMaterialMeshMap) + { + modelMaterialMeshes.push_back(kv.second); + } + + // Process texture loading for each MaterialMesh + std::vector combinedVertices; + std::vector combinedIndices; + + // Process texture loading for each MaterialMesh + for (auto &materialMesh : modelMaterialMeshes) + { + int materialIndex = materialMesh.materialIndex; + + // Get ALL texture paths for this material (same as ParseGLTFDataOnly) + if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) + { + const auto &gltfMaterial = gltfModel.materials[materialIndex]; + + // Extract base color texture + if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) + { + int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + int imageIndex = -1; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + imageIndex = texture.source; + } + else + { + auto extIt = texture.extensions.find("KHR_texture_basisu"); + if (extIt != texture.extensions.end()) + { + const tinygltf::Value &ext = extIt->second; + if (ext.Has("source") && ext.Get("source").IsInt()) + { + int src = ext.Get("source").Get(); + if (src >= 0 && src < static_cast(gltfModel.images.size())) + { + imageIndex = src; + } + } + } + } + if (imageIndex >= 0) + { + std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; // Keep for backward compatibility (now baseColor‑tagged) + + // Load texture data (embedded or external) with caching + const auto &image = gltfModel.images[imageIndex]; + if (!image.image.empty()) + { + if (!loadedTextures.contains(textureId)) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); + loadedTextures.insert(textureId); + std::cout << " Scheduled baseColor texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else + { + std::cout << " Using cached baseColor texture: " << textureId << std::endl; + } + } + else + { + std::cerr << " Warning: No decoded bytes for baseColor texture index " << texIndex << std::endl; + } + } + } + } + else + { + // Since texture indices are -1, try to find external texture files by material name + std::string materialName = materialMesh.materialName; + + // Look for external texture files that match this specific material (case-insensitive) + for (const auto &image : gltfModel.images) + { + if (!image.uri.empty()) + { + std::string imageUri = image.uri; + // Lowercase copies for robust matching + std::string imageUriLower = imageUri; + std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + std::string materialNameLower = materialName; + std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + // Check if this image belongs to this specific material based on naming patterns + // Look for basecolor/albedo/diffuse textures that match the material name + if ((imageUriLower.find("basecolor") != std::string::npos || + imageUriLower.find("albedo") != std::string::npos || + imageUriLower.find("diffuse") != std::string::npos) && + (imageUriLower.find(materialNameLower) != std::string::npos || + materialNameLower.find(imageUriLower.substr(0, imageUriLower.find('_'))) != std::string::npos)) + { + // Use the relative path from the GLTF directory + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Scheduled baseColor upload from memory (heuristic): " << textureId << std::endl; + } + else + { + // Fallback: offload KTX2 file load to renderer worker threads + renderer->LoadTextureAsync(textureId, true); + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Scheduled baseColor KTX2 load from file (heuristic): " << textureId << std::endl; + } + break; + } + } + } + } + + // Extract normal texture + if (gltfMaterial.normalTexture.index >= 0) + { + int texIndex = gltfMaterial.normalTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.normalTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Load embedded texture data + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded normal texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else if (!image.uri.empty()) + { + // Fallback: offload KTX2 normal map load to renderer worker threads + std::string filePath = baseTexturePath + image.uri; + renderer->RegisterTextureAlias(textureId, filePath); + renderer->LoadTextureAsync(filePath); + materialMesh.normalTexturePath = textureId; + std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; + } + } + } + } + else + { + // Heuristic: search images for a normal texture for this material and load from memory + std::string materialName = materialMesh.materialName; + for (const auto &image : gltfModel.images) + { + if (!image.uri.empty()) + { + std::string imageUri = image.uri; + if (imageUri.find("Normal") != std::string::npos && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) + { + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.normalTexturePath = textureId; + std::cout << " Scheduled normal upload from memory (heuristic): " << textureId << std::endl; + } + else + { + std::cerr << " Warning: Heuristic normal image has no decoded bytes: " << imageUri << std::endl; + } + break; + } + } + } + } + + // Extract metallic-roughness texture + if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) + { + int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.metallicRoughnessTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.metallicRoughnessTexturePath = textureId; + std::cout << " Scheduled metallic-roughness texture upload: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else + { + std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; + } + } + } + } + else + { + // Look for external metallic-roughness texture files that match this specific material + std::string materialName = materialMesh.materialName; + for (const auto &image : gltfModel.images) + { + if (!image.uri.empty()) + { + std::string imageUri = image.uri; + if ((imageUri.find("Metallic") != std::string::npos || + imageUri.find("Roughness") != std::string::npos || + imageUri.find("Specular") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) + { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.metallicRoughnessTexturePath = texturePath; + std::cout << " Found external metallic-roughness texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } + } + + // Extract occlusion texture + if (gltfMaterial.occlusionTexture.index >= 0) + { + int texIndex = gltfMaterial.occlusionTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.occlusionTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + if (renderer->LoadTextureFromMemory(textureId, image.image.data(), + image.width, image.height, image.component)) + { + materialMesh.occlusionTexturePath = textureId; + std::cout << " Loaded occlusion texture from memory: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else + { + std::cerr << " Failed to load occlusion texture from memory: " << textureId << std::endl; + } + } + else + { + std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; + } + } + } + } + else + { + // Heuristic: search images for an occlusion texture for this material and load from memory + std::string materialName = materialMesh.materialName; + for (const auto &image : gltfModel.images) + { + if (!image.uri.empty()) + { + std::string imageUri = image.uri; + if ((imageUri.find("Occlusion") != std::string::npos || + imageUri.find("AO") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) + { + std::string textureId = baseTexturePath + imageUri; + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.occlusionTexturePath = textureId; + std::cout << " Scheduled occlusion upload from memory (heuristic): " << textureId << std::endl; + } + else + { + std::cerr << " Warning: Heuristic occlusion image has no decoded bytes: " << imageUri << std::endl; + } + break; + } + } + } + } + + // Extract emissive texture + if (gltfMaterial.emissiveTexture.index >= 0) + { + int texIndex = gltfMaterial.emissiveTexture.index; + if (texIndex < gltfModel.textures.size()) + { + const auto &texture = gltfModel.textures[texIndex]; + if (texture.source >= 0 && texture.source < gltfModel.images.size()) + { + std::string textureId = "gltf_texture_" + std::to_string(texIndex); + materialMesh.emissiveTexturePath = textureId; + + // Load texture data (embedded or external) + const auto &image = gltfModel.images[texture.source]; + if (!image.image.empty()) + { + // Load embedded texture data + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + std::cout << " Scheduled embedded emissive texture: " << textureId + << " (" << image.width << "x" << image.height << ")" << std::endl; + } + else if (!image.uri.empty()) + { + // Record external texture file path (loaded later by renderer) + std::string texturePath = baseTexturePath + image.uri; + materialMesh.emissiveTexturePath = texturePath; + std::cout << " External emissive texture path: " << texturePath << std::endl; + } + } + } + } + else + { + // Look for external emissive texture files that match this specific material + std::string materialName = materialMesh.materialName; + for (const auto &image : gltfModel.images) + { + if (!image.uri.empty()) + { + std::string imageUri = image.uri; + if ((imageUri.find("Emissive") != std::string::npos || + imageUri.find("Emission") != std::string::npos) && + (imageUri.find(materialName) != std::string::npos || + materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) + { + std::string texturePath = baseTexturePath + imageUri; + materialMesh.emissiveTexturePath = texturePath; + std::cout << " Found external emissive texture for " << materialName << ": " << texturePath << std::endl; + break; + } + } + } + } + } + + // Add to combined mesh for backward compatibility (keep vertices in an original coordinate system) + if (!materialMesh.instances.empty()) + { + size_t vertexOffset = combinedVertices.size(); + + // Instance transforms should be handled by the instancing system, not applied to vertex data + for (const auto &vertex : materialMesh.vertices) + { + // Use vertices as-is without any transformation + combinedVertices.push_back(vertex); + } + + for (uint32_t index : materialMesh.indices) + { + combinedIndices.push_back(index + vertexOffset); + } + } + } + + // Store material meshes for this model + materialMeshes[filename] = modelMaterialMeshes; + + // Set the combined mesh data in the model for backward compatibility + model->SetVertices(combinedVertices); + model->SetIndices(combinedIndices); + + // Extract lights from the GLTF model + std::cout << "Extracting lights from GLTF model..." << std::endl; + + // Extract punctual lights (KHR_lights_punctual extension) + if (ExtractPunctualLights(gltfModel, filename)) + { + std::cerr << "Warning: Failed to extract punctual lights from " << filename << std::endl; + } + + std::cout << "GLTF model loaded successfully with " << combinedVertices.size() << " vertices and " << combinedIndices.size() << " indices" << std::endl; + return true; } - // Add all instances to this MaterialMesh (both new and existing geometry) - for (const glm::mat4& instanceTransform : instances) { - materialMesh.AddInstance(instanceTransform, static_cast(materialIndex)); - } - } - } - - // Convert geometry-based material mesh map to vector - std::vector modelMaterialMeshes; - for (auto& val : geometryMaterialMeshMap | std::views::values) { - modelMaterialMeshes.push_back(val); - } - - // Process texture loading for each MaterialMesh - std::vector combinedVertices; - std::vector combinedIndices; - - // Process texture loading for each MaterialMesh - for (auto & materialMesh : modelMaterialMeshes) { - int materialIndex = materialMesh.materialIndex; - - // Get ALL texture paths for this material (same as ParseGLTFDataOnly) - if (materialIndex >= 0 && materialIndex < gltfModel.materials.size()) { - const auto& gltfMaterial = gltfModel.materials[materialIndex]; - - // Extract base color texture - if (gltfMaterial.pbrMetallicRoughness.baseColorTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.baseColorTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - int imageIndex = -1; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - imageIndex = texture.source; - } else { - auto extIt = texture.extensions.find("KHR_texture_basisu"); - if (extIt != texture.extensions.end()) { - const tinygltf::Value& ext = extIt->second; - if (ext.Has("source") && ext.Get("source").IsInt()) { - int src = ext.Get("source").Get(); - if (src >= 0 && src < static_cast(gltfModel.images.size())) { - imageIndex = src; - } - } - } - } - if (imageIndex >= 0) { - std::string textureId = "gltf_baseColor_" + std::to_string(texIndex); - materialMesh.baseColorTexturePath = textureId; - materialMesh.texturePath = textureId; // Keep for backward compatibility (now baseColor‑tagged) - - // Load texture data (embedded or external) with caching - const auto& image = gltfModel.images[imageIndex]; - if (!image.image.empty()) { - if (!loadedTextures.contains(textureId)) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); - loadedTextures.insert(textureId); - std::cout << " Scheduled baseColor texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cout << " Using cached baseColor texture: " << textureId << std::endl; - } - } else { - std::cerr << " Warning: No decoded bytes for baseColor texture index " << texIndex << std::endl; - } - } - } - } else { - // Since texture indices are -1, try to find external texture files by material name - std::string materialName = materialMesh.materialName; - - // Look for external texture files that match this specific material (case-insensitive) - for (const auto & image : gltfModel.images) { - if (!image.uri.empty()) { - std::string imageUri = image.uri; - // Lowercase copies for robust matching - std::string imageUriLower = imageUri; - std::ranges::transform(imageUriLower, imageUriLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - std::string materialNameLower = materialName; - std::ranges::transform(materialNameLower, materialNameLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - - // Check if this image belongs to this specific material based on naming patterns - // Look for basecolor/albedo/diffuse textures that match the material name - if ((imageUriLower.find("basecolor") != std::string::npos || - imageUriLower.find("albedo") != std::string::npos || - imageUriLower.find("diffuse") != std::string::npos) && - (imageUriLower.find(materialNameLower) != std::string::npos || - materialNameLower.find(imageUriLower.substr(0, imageUriLower.find('_'))) != std::string::npos)) { - - // Use the relative path from the GLTF directory - std::string textureId = baseTexturePath + imageUri; - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - materialMesh.baseColorTexturePath = textureId; - materialMesh.texturePath = textureId; - std::cout << " Scheduled baseColor upload from memory (heuristic): " << textureId << std::endl; - } else { - // Fallback: offload KTX2 file load to renderer worker threads - renderer->LoadTextureAsync(textureId, true); - materialMesh.baseColorTexturePath = textureId; - materialMesh.texturePath = textureId; - std::cout << " Scheduled baseColor KTX2 load from file (heuristic): " << textureId << std::endl; - } - break; - } - } - } - } - - // Extract normal texture - if (gltfMaterial.normalTexture.index >= 0) { - int texIndex = gltfMaterial.normalTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - materialMesh.normalTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Load embedded texture data - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded normal texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else if (!image.uri.empty()) { - // Fallback: offload KTX2 normal map load to renderer worker threads - std::string filePath = baseTexturePath + image.uri; - renderer->RegisterTextureAlias(textureId, filePath); - renderer->LoadTextureAsync(filePath); - materialMesh.normalTexturePath = textureId; - std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for normal texture index " << texIndex << std::endl; - } - } - } - } else { - // Heuristic: search images for a normal texture for this material and load from memory - std::string materialName = materialMesh.materialName; - for (const auto & image : gltfModel.images) { - if (!image.uri.empty()) { - std::string imageUri = image.uri; - if (imageUri.find("Normal") != std::string::npos && - (imageUri.find(materialName) != std::string::npos || - materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string textureId = baseTexturePath + imageUri; - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - materialMesh.normalTexturePath = textureId; - std::cout << " Scheduled normal upload from memory (heuristic): " << textureId << std::endl; - } else { - std::cerr << " Warning: Heuristic normal image has no decoded bytes: " << imageUri << std::endl; - } - break; - } - } - } - } - - // Extract metallic-roughness texture - if (gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { - int texIndex = gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - materialMesh.metallicRoughnessTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - materialMesh.metallicRoughnessTexturePath = textureId; - std::cout << " Scheduled metallic-roughness texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; - } - } - } - } else { - // Look for external metallic-roughness texture files that match this specific material - std::string materialName = materialMesh.materialName; - for (const auto & image : gltfModel.images) { - if (!image.uri.empty()) { - std::string imageUri = image.uri; - if ((imageUri.find("Metallic") != std::string::npos || - imageUri.find("Roughness") != std::string::npos || - imageUri.find("Specular") != std::string::npos) && - (imageUri.find(materialName) != std::string::npos || - materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string texturePath = baseTexturePath + imageUri; - materialMesh.metallicRoughnessTexturePath = texturePath; - std::cout << " Found external metallic-roughness texture for " << materialName << ": " << texturePath << std::endl; - break; - } - } - } - } - - // Extract occlusion texture - if (gltfMaterial.occlusionTexture.index >= 0) { - int texIndex = gltfMaterial.occlusionTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - materialMesh.occlusionTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - if (renderer->LoadTextureFromMemory(textureId, image.image.data(), - image.width, image.height, image.component)) { - materialMesh.occlusionTexturePath = textureId; - std::cout << " Loaded occlusion texture from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else { - std::cerr << " Failed to load occlusion texture from memory: " << textureId << std::endl; - } - } else { - std::cerr << " Warning: No decoded bytes for occlusion texture index " << texIndex << std::endl; - } - } - } - } else { - // Heuristic: search images for an occlusion texture for this material and load from memory - std::string materialName = materialMesh.materialName; - for (const auto & image : gltfModel.images) { - if (!image.uri.empty()) { - std::string imageUri = image.uri; - if ((imageUri.find("Occlusion") != std::string::npos || - imageUri.find("AO") != std::string::npos) && - (imageUri.find(materialName) != std::string::npos || - materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string textureId = baseTexturePath + imageUri; - if (!image.image.empty()) { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - materialMesh.occlusionTexturePath = textureId; - std::cout << " Scheduled occlusion upload from memory (heuristic): " << textureId << std::endl; - } else { - std::cerr << " Warning: Heuristic occlusion image has no decoded bytes: " << imageUri << std::endl; - } - break; - } - } - } - } - - // Extract emissive texture - if (gltfMaterial.emissiveTexture.index >= 0) { - int texIndex = gltfMaterial.emissiveTexture.index; - if (texIndex < gltfModel.textures.size()) { - const auto& texture = gltfModel.textures[texIndex]; - if (texture.source >= 0 && texture.source < gltfModel.images.size()) { - std::string textureId = "gltf_texture_" + std::to_string(texIndex); - materialMesh.emissiveTexturePath = textureId; - - // Load texture data (embedded or external) - const auto& image = gltfModel.images[texture.source]; - if (!image.image.empty()) { - // Load embedded texture data - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded emissive texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } else if (!image.uri.empty()) { - // Record external texture file path (loaded later by renderer) - std::string texturePath = baseTexturePath + image.uri; - materialMesh.emissiveTexturePath = texturePath; - std::cout << " External emissive texture path: " << texturePath << std::endl; - } - } - } - } else { - // Look for external emissive texture files that match this specific material - std::string materialName = materialMesh.materialName; - for (const auto & image : gltfModel.images) { - if (!image.uri.empty()) { - std::string imageUri = image.uri; - if ((imageUri.find("Emissive") != std::string::npos || - imageUri.find("Emission") != std::string::npos) && - (imageUri.find(materialName) != std::string::npos || - materialName.find(imageUri.substr(0, imageUri.find('_'))) != std::string::npos)) { - std::string texturePath = baseTexturePath + imageUri; - materialMesh.emissiveTexturePath = texturePath; - std::cout << " Found external emissive texture for " << materialName << ": " << texturePath << std::endl; - break; - } - } - } - } - } - - // Add to combined mesh for backward compatibility (keep vertices in an original coordinate system) - if (!materialMesh.instances.empty()) { - size_t vertexOffset = combinedVertices.size(); - - // FIXED: Don't transform vertices - keep them in the original coordinate system - // Instance transforms should be handled by the instancing system, not applied to vertex data - for (const auto& vertex : materialMesh.vertices) { - // Use vertices as-is without any transformation - combinedVertices.push_back(vertex); - } - - for (uint32_t index : materialMesh.indices) { - combinedIndices.push_back(index + vertexOffset); - } - } - } - - // Store material meshes for this model - materialMeshes[filename] = modelMaterialMeshes; - - // Set the combined mesh data in the model for backward compatibility - model->SetVertices(combinedVertices); - model->SetIndices(combinedIndices); - - // Extract lights from the GLTF model - std::cout << "Extracting lights from GLTF model..." << std::endl; - - // Extract punctual lights (KHR_lights_punctual extension) - if (ExtractPunctualLights(gltfModel, filename)) { - std::cerr << "Warning: Failed to extract punctual lights from " << filename << std::endl; - } - - std::cout << "GLTF model loaded successfully with " << combinedVertices.size() << " vertices and " << combinedIndices.size() << " indices" << std::endl; - return true; +std::vector ModelLoader::GetExtractedLights(const std::string &modelName) const +{ + std::vector lights; + + // First, try to get punctual lights from the extracted lights storage + auto lightIt = extractedLights.find(modelName); + if (lightIt != extractedLights.end()) + { + lights = lightIt->second; + std::cout << "Found " << lights.size() << " punctual lights for model: " << modelName << std::endl; + } + + // Now extract emissive materials as light sources + auto materialMeshIt = materialMeshes.find(modelName); + if (materialMeshIt != materialMeshes.end()) + { + for (const auto &materialMesh : materialMeshIt->second) + { + // Get the material for this mesh + auto materialIt = materials.find(materialMesh.materialName); + if (materialIt != materials.end()) + { + const Material *material = materialIt->second.get(); + + // Check if this material has emissive properties (no threshold filtering) + float emissiveIntensity = glm::length(material->emissive) * material->emissiveStrength; + if (emissiveIntensity >= 0.1f) + { + // Calculate the center position and an approximate size of the emissive surface + glm::vec3 center(0.0f); + glm::vec3 minB(std::numeric_limits::max()); + glm::vec3 maxB(-std::numeric_limits::max()); + if (!materialMesh.vertices.empty()) + { + for (const auto &vertex : materialMesh.vertices) + { + center += vertex.position; + minB = glm::min(minB, vertex.position); + maxB = glm::max(maxB, vertex.position); + } + center /= static_cast(materialMesh.vertices.size()); + } + glm::vec3 extent = glm::max(maxB - minB, glm::vec3(0.0f)); + float diag = glm::length(extent); + float baseRange = std::max(0.5f * diag, 0.25f); // base range in local units + + // Calculate a reasonable direction (average normal of the surface) + glm::vec3 avgNormal(0.0f); + if (!materialMesh.vertices.empty()) + { + for (const auto &vertex : materialMesh.vertices) + { + avgNormal += vertex.normal; + } + avgNormal = glm::normalize(avgNormal / static_cast(materialMesh.vertices.size())); + } + else + { + avgNormal = glm::vec3(0.0f, -1.0f, 0.0f); // Default downward direction + } + + // Create emissive light(s) transformed by each instance's model matrix + if (!materialMesh.instances.empty()) + { + for (const auto &inst : materialMesh.instances) + { + glm::mat4 M = inst.getModelMatrix(); + glm::vec3 worldCenter = glm::vec3(M * glm::vec4(center, 1.0f)); + glm::mat3 normalMat = glm::transpose(glm::inverse(glm::mat3(M))); + glm::vec3 worldNormal = glm::normalize(normalMat * avgNormal); + + // Estimate a uniform scale factor from the instance transform + float sx = glm::length(glm::vec3(M[0])); + float sy = glm::length(glm::vec3(M[1])); + float sz = glm::length(glm::vec3(M[2])); + float sMax = std::max(sx, std::max(sy, sz)); + // Slightly conservative halo; avoid massive ranges that wash out the scene + float worldRange = baseRange * std::max(1.0f, sMax) * 1.25f; + + ExtractedLight emissiveLight; + emissiveLight.type = ExtractedLight::Type::Emissive; + emissiveLight.position = worldCenter; + // Separate chroma from intensity to avoid double-powering color and intensity + glm::vec3 chroma = material->emissive; + float chromaMag = glm::length(chroma); + emissiveLight.color = (chromaMag > 1e-6f) ? (chroma / chromaMag) : chroma; + float strength = hasEmissiveStrengthExtension ? material->emissiveStrength : 1.0f; + // Use a surface-area proxy from local bounds (diag^2) scaled by instance size, not range^2 + float areaProxy = std::max(diag * diag * std::max(1.0f, sMax), 0.01f); + float intensityRaw = strength * chromaMag * areaProxy * 0.08f; // conservative scalar + // Clamp to a reasonable band to avoid blowing out exposure + emissiveLight.intensity = glm::clamp(intensityRaw, 0.25f, 50.0f); + emissiveLight.range = worldRange; + emissiveLight.sourceMaterial = material->GetName(); + emissiveLight.direction = worldNormal; + + lights.push_back(emissiveLight); + + std::cout << "Created emissive light from material '" << material->GetName() + << "' at world position (" << worldCenter.x << ", " << worldCenter.y << ", " << worldCenter.z + << ") with intensity " << emissiveIntensity << std::endl; + } + } + else + { + // No explicit instances; use identity transform + ExtractedLight emissiveLight; + emissiveLight.type = ExtractedLight::Type::Emissive; + emissiveLight.position = center; + // Separate chroma from intensity + glm::vec3 chroma = material->emissive; + float chromaMag = glm::length(chroma); + emissiveLight.color = (chromaMag > 1e-6f) ? (chroma / chromaMag) : chroma; + float strength = hasEmissiveStrengthExtension ? material->emissiveStrength : 1.0f; + float worldRange = baseRange * 1.25f; + float areaProxy = std::max(diag * diag, 0.01f); + float intensityRaw = strength * chromaMag * areaProxy * 0.08f; + emissiveLight.intensity = glm::clamp(intensityRaw, 0.25f, 50.0f); + emissiveLight.range = worldRange; + emissiveLight.sourceMaterial = material->GetName(); + emissiveLight.direction = avgNormal; + + lights.push_back(emissiveLight); + + std::cout << "Created emissive light from material '" << material->GetName() + << "' at position (" << center.x << ", " << center.y << ", " << center.z + << ") with intensity " << emissiveIntensity << std::endl; + } + } + } + } + } + + std::cout << "Total lights extracted for model '" << modelName << "': " << lights.size() + << " (including emissive-derived lights)" << std::endl; + + return lights; } -std::vector ModelLoader::GetExtractedLights(const std::string& modelName) const { - std::vector lights; - - // First, try to get punctual lights from the extracted lights storage - auto lightIt = extractedLights.find(modelName); - if (lightIt != extractedLights.end()) { - lights = lightIt->second; - std::cout << "Found " << lights.size() << " punctual lights for model: " << modelName << std::endl; - } - - // Now extract emissive materials as light sources - auto materialMeshIt = materialMeshes.find(modelName); - if (materialMeshIt != materialMeshes.end()) { - for (const auto& materialMesh : materialMeshIt->second) { - // Get the material for this mesh - auto materialIt = materials.find(materialMesh.materialName); - if (materialIt != materials.end()) { - const Material* material = materialIt->second.get(); - - // Check if this material has emissive properties (no threshold filtering) - float emissiveIntensity = glm::length(material->emissive) * material->emissiveStrength; - if (emissiveIntensity >= 0.1f) { - // Calculate the center position of the emissive surface - glm::vec3 center(0.0f); - if (!materialMesh.vertices.empty()) { - for (const auto& vertex : materialMesh.vertices) { - center += vertex.position; - } - center /= static_cast(materialMesh.vertices.size()); - } - - // Calculate a reasonable direction (average normal of the surface) - glm::vec3 avgNormal(0.0f); - if (!materialMesh.vertices.empty()) { - for (const auto& vertex : materialMesh.vertices) { - avgNormal += vertex.normal; - } - avgNormal = glm::normalize(avgNormal / static_cast(materialMesh.vertices.size())); - } else { - avgNormal = glm::vec3(0.0f, -1.0f, 0.0f); // Default downward direction - } - - // Create emissive light(s) transformed by each instance's model matrix - if (!materialMesh.instances.empty()) { - for (const auto& inst : materialMesh.instances) { - glm::mat4 M = inst.getModelMatrix(); - glm::vec3 worldCenter = glm::vec3(M * glm::vec4(center, 1.0f)); - glm::mat3 normalMat = glm::transpose(glm::inverse(glm::mat3(M))); - glm::vec3 worldNormal = glm::normalize(normalMat * avgNormal); - - ExtractedLight emissiveLight; - emissiveLight.type = ExtractedLight::Type::Emissive; - emissiveLight.position = worldCenter; - emissiveLight.color = material->emissive; - emissiveLight.intensity = (hasEmissiveStrengthExtension)?material->emissiveStrength:1.0f; - emissiveLight.range = 1.0f; // Default range for emissive lights - emissiveLight.sourceMaterial = material->GetName(); - emissiveLight.direction = worldNormal; - - lights.push_back(emissiveLight); - - std::cout << "Created emissive light from material '" << material->GetName() - << "' at world position (" << worldCenter.x << ", " << worldCenter.y << ", " << worldCenter.z - << ") with intensity " << emissiveIntensity << std::endl; - } - } else { - // No explicit instances; use identity transform - ExtractedLight emissiveLight; - emissiveLight.type = ExtractedLight::Type::Emissive; - emissiveLight.position = center; - emissiveLight.color = material->emissive; - emissiveLight.intensity = hasEmissiveStrengthExtension?material->emissiveStrength:1.0f; - emissiveLight.range = 1.0f; // Default range for emissive lights - emissiveLight.sourceMaterial = material->GetName(); - emissiveLight.direction = avgNormal; - - lights.push_back(emissiveLight); - - std::cout << "Created emissive light from material '" << material->GetName() - << "' at position (" << center.x << ", " << center.y << ", " << center.z - << ") with intensity " << emissiveIntensity << std::endl; - } - } - } - } - } - - std::cout << "Total lights extracted for model '" << modelName << "': " << lights.size() - << " (including emissive-derived lights)" << std::endl; - - return lights; +const std::vector &ModelLoader::GetMaterialMeshes(const std::string &modelName) const +{ + auto it = materialMeshes.find(modelName); + if (it != materialMeshes.end()) + { + return it->second; + } + // Return a static empty vector to avoid creating temporary objects. + static const std::vector emptyVector; + return emptyVector; } -const std::vector& ModelLoader::GetMaterialMeshes(const std::string& modelName) const { - auto it = materialMeshes.find(modelName); - if (it != materialMeshes.end()) { - return it->second; - } - // Return a static empty vector to avoid creating temporary objects - static constexpr std::vector emptyVector; - return emptyVector; +Material *ModelLoader::GetMaterial(const std::string &materialName) const +{ + auto it = materials.find(materialName); + if (it != materials.end()) + { + return it->second.get(); + } + return nullptr; } -Material* ModelLoader::GetMaterial(const std::string& materialName) const { - auto it = materials.find(materialName); - if (it != materials.end()) { - return it->second.get(); - } - return nullptr; +const std::vector &ModelLoader::GetAnimations(const std::string &modelName) const +{ + auto it = models.find(modelName); + if (it != models.end() && it->second) + { + return it->second->GetAnimations(); + } + // Return a static empty vector to avoid creating temporary objects. + static const std::vector emptyVector; + return emptyVector; } -bool ModelLoader::ExtractPunctualLights(const tinygltf::Model& gltfModel, const std::string& modelName) { - std::cout << "Extracting punctual lights from model: " << modelName << std::endl; - - std::vector lights; - - // Check if the model has the KHR_lights_punctual extension - auto extensionIt = gltfModel.extensions.find("KHR_lights_punctual"); - if (extensionIt != gltfModel.extensions.end()) { - std::cout << " Found KHR_lights_punctual extension" << std::endl; - - // Parse the punctual lights from the extension - const tinygltf::Value& extension = extensionIt->second; - if (extension.Has("lights") && extension.Get("lights").IsArray()) { - const tinygltf::Value::Array& lightsArray = extension.Get("lights").Get(); - - for (size_t i = 0; i < lightsArray.size(); ++i) { - const tinygltf::Value& lightValue = lightsArray[i]; - if (!lightValue.IsObject()) continue; - - ExtractedLight light; - - // Parse light type - if (lightValue.Has("type") && lightValue.Get("type").IsString()) { - std::string type = lightValue.Get("type").Get(); - if (type == "directional") { - light.type = ExtractedLight::Type::Directional; - } else if (type == "point") { - light.type = ExtractedLight::Type::Point; - } else if (type == "spot") { - light.type = ExtractedLight::Type::Spot; - } - } - - // Parse light color - if (lightValue.Has("color") && lightValue.Get("color").IsArray()) { - const tinygltf::Value::Array& colorArray = lightValue.Get("color").Get(); - if (colorArray.size() >= 3) { - light.color = glm::vec3( - colorArray[0].IsNumber() ? static_cast(colorArray[0].Get()) : 1.0f, - colorArray[1].IsNumber() ? static_cast(colorArray[1].Get()) : 1.0f, - colorArray[2].IsNumber() ? static_cast(colorArray[2].Get()) : 1.0f - ); - } - } - - // Parse light intensity - if (lightValue.Has("intensity") && lightValue.Get("intensity").IsNumber()) { - light.intensity = static_cast(lightValue.Get("intensity").Get()) * LIGHT_SCALE_FACTOR; - } - - // Parse light range (for point and spotlights) - if (lightValue.Has("range") && lightValue.Get("range").IsNumber()) { - light.range = static_cast(lightValue.Get("range").Get()); - } - - // Parse spotlights specific parameters - if (light.type == ExtractedLight::Type::Spot && lightValue.Has("spot")) { - const tinygltf::Value& spotValue = lightValue.Get("spot"); - if (spotValue.Has("innerConeAngle") && spotValue.Get("innerConeAngle").IsNumber()) { - light.innerConeAngle = static_cast(spotValue.Get("innerConeAngle").Get()); - } - if (spotValue.Has("outerConeAngle") && spotValue.Get("outerConeAngle").IsNumber()) { - light.outerConeAngle = static_cast(spotValue.Get("outerConeAngle").Get()); - } - } - - lights.push_back(light); - std::cout << " Parsed punctual light " << i << ": type=" << static_cast(light.type) - << ", intensity=" << light.intensity << std::endl; - } - } - } else { - std::cout << " No KHR_lights_punctual extension found" << std::endl; - } - - // Compute world transforms for all nodes in the default scene - std::vector nodeWorldTransforms(gltfModel.nodes.size(), glm::mat4(1.0f)); - - auto calcLocal = [](const tinygltf::Node& n) -> glm::mat4 { - // If matrix is provided, use it - if (n.matrix.size() == 16) { - glm::mat4 m(1.0f); - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - m[c][r] = static_cast(n.matrix[r * 4 + c]); - } - } - return m; - } - // Otherwise compose TRS - glm::mat4 T(1.0f), R(1.0f), S(1.0f); - if (n.translation.size() == 3) { - T = glm::translate(glm::mat4(1.0f), glm::vec3( - static_cast(n.translation[0]), - static_cast(n.translation[1]), - static_cast(n.translation[2]))); - } - if (n.rotation.size() == 4) { - glm::quat q( - static_cast(n.rotation[3]), - static_cast(n.rotation[0]), - static_cast(n.rotation[1]), - static_cast(n.rotation[2])); - R = glm::mat4_cast(q); - } - if (n.scale.size() == 3) { - S = glm::scale(glm::mat4(1.0f), glm::vec3( - static_cast(n.scale[0]), - static_cast(n.scale[1]), - static_cast(n.scale[2]))); - } - return T * R * S; - }; - - std::function traverseNode = [&](int nodeIndex, const glm::mat4& parent) { - if (nodeIndex < 0 || nodeIndex >= static_cast(gltfModel.nodes.size())) return; - const tinygltf::Node& n = gltfModel.nodes[nodeIndex]; - glm::mat4 local = calcLocal(n); - glm::mat4 world = parent * local; - nodeWorldTransforms[nodeIndex] = world; - for (int child : n.children) { - traverseNode(child, world); - } - }; - - if (!gltfModel.scenes.empty()) { - int sceneIndex = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; - if (sceneIndex < static_cast(gltfModel.scenes.size())) { - const tinygltf::Scene& scene = gltfModel.scenes[sceneIndex]; - for (int root : scene.nodes) { - traverseNode(root, glm::mat4(1.0f)); - } - } - } else { - // Fallback: traverse all nodes as roots - for (int i = 0; i < static_cast(gltfModel.nodes.size()); ++i) { - traverseNode(i, glm::mat4(1.0f)); - } - } - - // Now assign positions and directions using world transforms - for (size_t nodeIndex = 0; nodeIndex < gltfModel.nodes.size(); ++nodeIndex) { - const auto& node = gltfModel.nodes[nodeIndex]; - if (node.extensions.contains("KHR_lights_punctual")) { - const tinygltf::Value& nodeExtension = node.extensions.at("KHR_lights_punctual"); - if (nodeExtension.Has("light") && nodeExtension.Get("light").IsInt()) { - int lightIndex = nodeExtension.Get("light").Get(); - if (lightIndex >= 0 && lightIndex < static_cast(lights.size())) { - const glm::mat4& W = nodeWorldTransforms[nodeIndex]; - // Position from world transform origin - glm::vec3 pos = glm::vec3(W * glm::vec4(0, 0, 0, 1)); - lights[lightIndex].position = pos; - - // Direction for directional/spot: transform -Z - if (lights[lightIndex].type == ExtractedLight::Type::Directional || - lights[lightIndex].type == ExtractedLight::Type::Spot) { - glm::mat3 rot = glm::mat3(W); - glm::vec3 dir = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f)); - lights[lightIndex].direction = dir; - } - - std::cout << " Light " << lightIndex << " positioned at (" - << lights[lightIndex].position.x << ", " - << lights[lightIndex].position.y << ", " - << lights[lightIndex].position.z << ")" << std::endl; - } - } - } - } - - // Store the extracted lights - extractedLights[modelName] = lights; - - std::cout << " Extracted " << lights.size() << " total lights from model" << std::endl; - return lights.empty(); +bool ModelLoader::ExtractPunctualLights(const tinygltf::Model &gltfModel, const std::string &modelName) +{ + std::cout << "Extracting punctual lights from model: " << modelName << std::endl; + + std::vector lights; + + // Check if the model has the KHR_lights_punctual extension + auto extensionIt = gltfModel.extensions.find("KHR_lights_punctual"); + if (extensionIt != gltfModel.extensions.end()) + { + std::cout << " Found KHR_lights_punctual extension" << std::endl; + + // Parse the punctual lights from the extension + const tinygltf::Value &extension = extensionIt->second; + if (extension.Has("lights") && extension.Get("lights").IsArray()) + { + const tinygltf::Value::Array &lightsArray = extension.Get("lights").Get(); + + for (size_t i = 0; i < lightsArray.size(); ++i) + { + const tinygltf::Value &lightValue = lightsArray[i]; + if (!lightValue.IsObject()) + continue; + + ExtractedLight light; + + // Parse light type + if (lightValue.Has("type") && lightValue.Get("type").IsString()) + { + std::string type = lightValue.Get("type").Get(); + if (type == "directional") + { + light.type = ExtractedLight::Type::Directional; + } + else if (type == "point") + { + light.type = ExtractedLight::Type::Point; + } + else if (type == "spot") + { + light.type = ExtractedLight::Type::Spot; + } + } + + // Parse light color + if (lightValue.Has("color") && lightValue.Get("color").IsArray()) + { + const tinygltf::Value::Array &colorArray = lightValue.Get("color").Get(); + if (colorArray.size() >= 3) + { + light.color = glm::vec3( + colorArray[0].IsNumber() ? static_cast(colorArray[0].Get()) : 1.0f, + colorArray[1].IsNumber() ? static_cast(colorArray[1].Get()) : 1.0f, + colorArray[2].IsNumber() ? static_cast(colorArray[2].Get()) : 1.0f); + } + } + + // Parse light intensity + if (lightValue.Has("intensity") && lightValue.Get("intensity").IsNumber()) + { + light.intensity = static_cast(lightValue.Get("intensity").Get()) * LIGHT_SCALE_FACTOR; + } + + // Parse light range (for point and spotlights) + if (lightValue.Has("range") && lightValue.Get("range").IsNumber()) + { + light.range = static_cast(lightValue.Get("range").Get()); + } + + // Parse spotlights specific parameters + if (light.type == ExtractedLight::Type::Spot && lightValue.Has("spot")) + { + const tinygltf::Value &spotValue = lightValue.Get("spot"); + if (spotValue.Has("innerConeAngle") && spotValue.Get("innerConeAngle").IsNumber()) + { + light.innerConeAngle = static_cast(spotValue.Get("innerConeAngle").Get()); + } + if (spotValue.Has("outerConeAngle") && spotValue.Get("outerConeAngle").IsNumber()) + { + light.outerConeAngle = static_cast(spotValue.Get("outerConeAngle").Get()); + } + } + + lights.push_back(light); + std::cout << " Parsed punctual light " << i << ": type=" << static_cast(light.type) + << ", intensity=" << light.intensity << std::endl; + } + } + } + else + { + std::cout << " No KHR_lights_punctual extension found" << std::endl; + } + + // Compute world transforms for all nodes in the default scene + std::vector nodeWorldTransforms(gltfModel.nodes.size(), glm::mat4(1.0f)); + + auto calcLocal = [](const tinygltf::Node &n) -> glm::mat4 { + // If matrix is provided, use it + if (n.matrix.size() == 16) + { + glm::mat4 m(1.0f); + for (int r = 0; r < 4; ++r) + { + for (int c = 0; c < 4; ++c) + { + m[c][r] = static_cast(n.matrix[r * 4 + c]); + } + } + return m; + } + // Otherwise compose TRS + glm::mat4 T(1.0f), R(1.0f), S(1.0f); + if (n.translation.size() == 3) + { + T = glm::translate(glm::mat4(1.0f), glm::vec3( + static_cast(n.translation[0]), + static_cast(n.translation[1]), + static_cast(n.translation[2]))); + } + if (n.rotation.size() == 4) + { + glm::quat q( + static_cast(n.rotation[3]), + static_cast(n.rotation[0]), + static_cast(n.rotation[1]), + static_cast(n.rotation[2])); + R = glm::mat4_cast(q); + } + if (n.scale.size() == 3) + { + S = glm::scale(glm::mat4(1.0f), glm::vec3( + static_cast(n.scale[0]), + static_cast(n.scale[1]), + static_cast(n.scale[2]))); + } + return T * R * S; + }; + + std::function traverseNode = [&](int nodeIndex, const glm::mat4 &parent) { + if (nodeIndex < 0 || nodeIndex >= static_cast(gltfModel.nodes.size())) + return; + const tinygltf::Node &n = gltfModel.nodes[nodeIndex]; + glm::mat4 local = calcLocal(n); + glm::mat4 world = parent * local; + nodeWorldTransforms[nodeIndex] = world; + for (int child : n.children) + { + traverseNode(child, world); + } + }; + + if (!gltfModel.scenes.empty()) + { + int sceneIndex = gltfModel.defaultScene >= 0 ? gltfModel.defaultScene : 0; + if (sceneIndex < static_cast(gltfModel.scenes.size())) + { + const tinygltf::Scene &scene = gltfModel.scenes[sceneIndex]; + for (int root : scene.nodes) + { + traverseNode(root, glm::mat4(1.0f)); + } + } + } + else + { + // Fallback: traverse all nodes as roots + for (int i = 0; i < static_cast(gltfModel.nodes.size()); ++i) + { + traverseNode(i, glm::mat4(1.0f)); + } + } + + // Now assign positions and directions using world transforms + for (size_t nodeIndex = 0; nodeIndex < gltfModel.nodes.size(); ++nodeIndex) + { + const auto &node = gltfModel.nodes[nodeIndex]; + if (node.extensions.contains("KHR_lights_punctual")) + { + const tinygltf::Value &nodeExtension = node.extensions.at("KHR_lights_punctual"); + if (nodeExtension.Has("light") && nodeExtension.Get("light").IsInt()) + { + int lightIndex = nodeExtension.Get("light").Get(); + if (lightIndex >= 0 && lightIndex < static_cast(lights.size())) + { + const glm::mat4 &W = nodeWorldTransforms[nodeIndex]; + // Position from world transform origin + glm::vec3 pos = glm::vec3(W * glm::vec4(0, 0, 0, 1)); + lights[lightIndex].position = pos; + + // Direction for directional/spot: transform -Z + if (lights[lightIndex].type == ExtractedLight::Type::Directional || + lights[lightIndex].type == ExtractedLight::Type::Spot) + { + glm::mat3 rot = glm::mat3(W); + glm::vec3 dir = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f)); + lights[lightIndex].direction = dir; + } + + std::cout << " Light " << lightIndex << " positioned at (" + << lights[lightIndex].position.x << ", " + << lights[lightIndex].position.y << ", " + << lights[lightIndex].position.z << ")" << std::endl; + } + } + } + } + + // Store the extracted lights + extractedLights[modelName] = lights; + + std::cout << " Extracted " << lights.size() << " total lights from model" << std::endl; + return lights.empty(); } diff --git a/attachments/simple_engine/model_loader.h b/attachments/simple_engine/model_loader.h index 67a0947a..596fc155 100644 --- a/attachments/simple_engine/model_loader.h +++ b/attachments/simple_engine/model_loader.h @@ -1,299 +1,462 @@ +/* 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 "mesh_component.h" #include #include -#include "mesh_component.h" +#include #include +#include +#include +#include +#include class Renderer; class Mesh; class Material; // Forward declaration for tinygltf -namespace tinygltf { - class Model; +namespace tinygltf +{ +class Model; } -class Material { - public: - explicit Material(std::string name) : name(std::move(name)) {} - ~Material() = default; - - [[nodiscard]] const std::string& GetName() const { return name; } - - // PBR properties (Metallic-Roughness default) - glm::vec3 albedo = glm::vec3(1.0f); - float metallic = 0.0f; - float roughness = 1.0f; - float ao = 1.0f; - glm::vec3 emissive = glm::vec3(0.0f); - float ior = 1.5f; // Index of refraction - float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension - float alpha = 1.0f; // Base color alpha (from MR baseColorFactor or SpecGloss diffuseFactor) - float transmissionFactor = 0.0f; // KHR_materials_transmission: 0=opaque, 1=fully transmissive - - // Specular-Glossiness workflow (KHR_materials_pbrSpecularGlossiness) - bool useSpecularGlossiness = false; - glm::vec3 specularFactor = glm::vec3(0.04f); - float glossinessFactor = 1.0f; - std::string specGlossTexturePath; // Stored separately; also mirrored to metallicRoughnessTexturePath for binding 2 - - // Alpha handling (glTF alphaMode and cutoff) - std::string alphaMode = "OPAQUE"; // "OPAQUE", "MASK", or "BLEND" - float alphaCutoff = 0.5f; // Used when alphaMode == MASK - - // Texture paths for PBR materials - std::string albedoTexturePath; - std::string normalTexturePath; - std::string metallicRoughnessTexturePath; - std::string occlusionTexturePath; - std::string emissiveTexturePath; - - // Hint used by the renderer to select a specialized glass rendering path - // for architectural glass (windows, lamp glass, etc.). Set by ModelLoader - // based on material name/properties; defaults to false so non-glass - // materials continue to use the generic PBR path. - bool isGlass = false; - - // Hint used by the renderer to preferentially render inner liquid volumes - // before outer glass shells (e.g., beer/wine in bar glasses). Set by - // ModelLoader based on material name/properties; defaults to false. - bool isLiquid = false; - - private: - std::string name; +class Material +{ + public: + explicit Material(std::string name) : + name(std::move(name)) + {} + ~Material() = default; + + [[nodiscard]] const std::string &GetName() const + { + return name; + } + + // PBR properties (Metallic-Roughness default) + glm::vec3 albedo = glm::vec3(1.0f); + float metallic = 0.0f; + float roughness = 1.0f; + float ao = 1.0f; + glm::vec3 emissive = glm::vec3(0.0f); + float ior = 1.5f; // Index of refraction + float emissiveStrength = 1.0f; // KHR_materials_emissive_strength extension + float alpha = 1.0f; // Base color alpha (from MR baseColorFactor or SpecGloss diffuseFactor) + float transmissionFactor = 0.0f; // KHR_materials_transmission: 0=opaque, 1=fully transmissive + + // Specular-Glossiness workflow (KHR_materials_pbrSpecularGlossiness) + bool useSpecularGlossiness = false; + glm::vec3 specularFactor = glm::vec3(0.04f); + float glossinessFactor = 1.0f; + std::string specGlossTexturePath; // Stored separately; also mirrored to metallicRoughnessTexturePath for binding 2 + + // Alpha handling (glTF alphaMode and cutoff) + std::string alphaMode = "OPAQUE"; // "OPAQUE", "MASK", or "BLEND" + float alphaCutoff = 0.5f; // Used when alphaMode == MASK + + // Texture paths for PBR materials + std::string albedoTexturePath; + std::string normalTexturePath; + std::string metallicRoughnessTexturePath; + std::string occlusionTexturePath; + std::string emissiveTexturePath; + + // Hint used by the renderer to select a specialized glass rendering path + // for architectural glass (windows, lamp glass, etc.). Set by ModelLoader + // based on material name/properties; defaults to false so non-glass + // materials continue to use the generic PBR path. + bool isGlass = false; + + // Hint used by the renderer to preferentially render inner liquid volumes + // before outer glass shells (e.g., beer/wine in bar glasses). Set by + // ModelLoader based on material name/properties; defaults to false. + bool isLiquid = false; + + private: + std::string name; }; - /** * @brief Structure representing a light source extracted from GLTF. */ -struct ExtractedLight { - enum class Type { - Directional, - Point, - Spot, - Emissive // Light derived from emissive material - }; - - Type type = Type::Point; - glm::vec3 position = glm::vec3(0.0f); - glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // For directional/spotlights - glm::vec3 color = glm::vec3(1.0f); - float intensity = 1.0f; - float range = 100.0f; // For point/spotlights - float innerConeAngle = 0.0f; // For spotlights - float outerConeAngle = 0.785398f; // For spotlights (45 degrees) - std::string sourceMaterial; // Name of source material (for emissive lights) +struct ExtractedLight +{ + enum class Type + { + Directional, + Point, + Spot, + Emissive // Light derived from emissive material + }; + + Type type = Type::Point; + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // For directional/spotlights + glm::vec3 color = glm::vec3(1.0f); + float intensity = 1.0f; + float range = 100.0f; // For point/spotlights + float innerConeAngle = 0.0f; // For spotlights + float outerConeAngle = 0.785398f; // For spotlights (45 degrees) + std::string sourceMaterial; // Name of source material (for emissive lights) }; /** * @brief Structure representing camera data extracted from GLTF. */ -struct CameraData { - std::string name; - bool isPerspective = true; +struct CameraData +{ + std::string name; + bool isPerspective = true; - // Perspective camera properties - float fov = 0.785398f; // 45 degrees in radians - float aspectRatio = 1.0f; + // Perspective camera properties + float fov = 0.785398f; // 45 degrees in radians + float aspectRatio = 1.0f; - // Orthographic camera properties - float orthographicSize = 1.0f; + // Orthographic camera properties + float orthographicSize = 1.0f; - // Common properties - float nearPlane = 0.1f; - float farPlane = 1000.0f; + // Common properties + float nearPlane = 0.1f; + float farPlane = 1000.0f; - // Transform properties - glm::vec3 position = glm::vec3(0.0f); - glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + // Transform properties + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion }; /** - * @brief Structure representing mesh data for a specific material. + * @brief Interpolation type for animation samplers. */ -struct MaterialMesh { - int materialIndex; - std::string materialName; - std::vector vertices; - std::vector indices; - - // All PBR texture paths for this material - std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility - std::string baseColorTexturePath; // Base color (albedo) texture - std::string normalTexturePath; // Normal map texture - std::string metallicRoughnessTexturePath; // Metallic-roughness texture - std::string occlusionTexturePath; // Ambient occlusion texture - std::string emissiveTexturePath; // Emissive texture - - // Instancing support - std::vector instances; // Instance data for instanced rendering - bool isInstanced = false; // Flag to indicate if this mesh uses instancing - - /** - * @brief Add an instance with the given transform matrix. - * @param transform The transform matrix for this instance. - * @param matIndex The material index for this instance (default: use materialIndex). - */ - void AddInstance(const glm::mat4& transform, uint32_t matIndex = 0) { - if (matIndex == 0) matIndex = static_cast(materialIndex); - instances.emplace_back(transform, matIndex); - isInstanced = instances.size() > 1; - } - - /** - * @brief Get the number of instances. - * @return Number of instances (0 if not instanced, >= 1 if instanced). - */ - [[nodiscard]] size_t GetInstanceCount() const { - return instances.size(); - } - - /** - * @brief Check if this mesh uses instancing. - * @return True if instanced (more than 1 instance), false otherwise. - */ - [[nodiscard]] bool IsInstanced() const { - return isInstanced; - } +enum class AnimationInterpolation +{ + Linear, + Step, + CubicSpline }; /** - * @brief Class representing a 3D model. + * @brief Target path for animation channels. */ -class Model { -public: - explicit Model(std::string name) : name(std::move(name)) {} - ~Model() = default; - - [[nodiscard]] const std::string& GetName() const { return name; } +enum class AnimationPath +{ + Translation, + Rotation, + Scale, + Weights // For morph targets (not yet implemented) +}; - // Mesh data access methods - [[nodiscard]] const std::vector& GetVertices() const { return vertices; } - [[nodiscard]] const std::vector& GetIndices() const { return indices; } +/** + * @brief Sampler for animation keyframes. + * Contains input (time) and output (value) data for interpolation. + */ +struct AnimationSampler +{ + std::vector inputTimes; // Keyframe timestamps in seconds + std::vector outputValues; // Keyframe values (vec3 for T/S, vec4 for R) + AnimationInterpolation interpolation = AnimationInterpolation::Linear; + + // Get the duration of this sampler + [[nodiscard]] float GetDuration() const + { + return inputTimes.empty() ? 0.0f : inputTimes.back(); + } +}; - // Methods to set mesh data (used by parser) - void SetVertices(const std::vector& newVertices) { vertices = newVertices; } - void SetIndices(const std::vector& newIndices) { indices = newIndices; } +/** + * @brief Channel connecting a sampler to a target node property. + */ +struct AnimationChannel +{ + int samplerIndex = -1; // Index into Animation::samplers + int targetNode = -1; // glTF node index being animated + AnimationPath path = AnimationPath::Translation; +}; - // Camera data access methods - [[nodiscard]] const std::vector& GetCameras() const { return cameras; } +/** + * @brief A complete animation clip containing multiple channels. + */ +struct Animation +{ + std::string name; + std::vector samplers; + std::vector channels; + + // Get the total duration of this animation + [[nodiscard]] float GetDuration() const + { + float maxDuration = 0.0f; + for (const auto &sampler : samplers) + { + maxDuration = std::max(maxDuration, sampler.GetDuration()); + } + return maxDuration; + } +}; - std::vector cameras; +/** + * @brief Structure representing mesh data for a specific material. + */ +struct MaterialMesh +{ + int materialIndex; + std::string materialName; + std::vector vertices; + std::vector indices; + + // Track which glTF mesh index this MaterialMesh came from (for animation targeting) + int sourceMeshIndex = -1; + + // All PBR texture paths for this material + std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility + std::string baseColorTexturePath; // Base color (albedo) texture + std::string normalTexturePath; // Normal map texture + std::string metallicRoughnessTexturePath; // Metallic-roughness texture + std::string occlusionTexturePath; // Ambient occlusion texture + std::string emissiveTexturePath; // Emissive texture + + // Instancing support + std::vector instances; // Instance data for instanced rendering + bool isInstanced = false; // Flag to indicate if this mesh uses instancing + + /** + * @brief Add an instance with the given transform matrix. + * @param transform The transform matrix for this instance. + * @param matIndex The material index for this instance (default: use materialIndex). + */ + void AddInstance(const glm::mat4 &transform, uint32_t matIndex = 0) + { + if (matIndex == 0) + matIndex = static_cast(materialIndex); + instances.emplace_back(transform, matIndex); + isInstanced = instances.size() > 1; + } + + /** + * @brief Get the number of instances. + * @return Number of instances (0 if not instanced, >= 1 if instanced). + */ + [[nodiscard]] size_t GetInstanceCount() const + { + return instances.size(); + } + + /** + * @brief Check if this mesh uses instancing. + * @return True if instanced (more than 1 instance), false otherwise. + */ + [[nodiscard]] bool IsInstanced() const + { + return isInstanced; + } +}; -private: - std::string name; - std::vector vertices; - std::vector indices; - // Other model data (meshes, materials, etc.) +/** + * @brief Class representing a 3D model. + */ +class Model +{ + public: + explicit Model(std::string name) : + name(std::move(name)) + {} + ~Model() = default; + + [[nodiscard]] const std::string &GetName() const + { + return name; + } + + // Mesh data access methods + [[nodiscard]] const std::vector &GetVertices() const + { + return vertices; + } + [[nodiscard]] const std::vector &GetIndices() const + { + return indices; + } + + // Methods to set mesh data (used by parser) + void SetVertices(const std::vector &newVertices) + { + vertices = newVertices; + } + void SetIndices(const std::vector &newIndices) + { + indices = newIndices; + } + + // Camera data access methods + [[nodiscard]] const std::vector &GetCameras() const + { + return cameras; + } + + // Animation data access methods + [[nodiscard]] const std::vector &GetAnimations() const + { + return animations; + } + void SetAnimations(const std::vector &anims) + { + animations = anims; + } + + // Animated node transforms: maps glTF node index to its base world transform + // Used by AnimationComponent to find entities for animation targets + [[nodiscard]] const std::unordered_map &GetAnimatedNodeTransforms() const + { + return animatedNodeTransforms; + } + void SetAnimatedNodeTransforms(const std::unordered_map &transforms) + { + animatedNodeTransforms = transforms; + } + + // Animated node to mesh mapping: maps glTF node index to mesh index + // Used to link animated nodes to their geometry entities + [[nodiscard]] const std::unordered_map &GetAnimatedNodeMeshes() const + { + return animatedNodeMeshes; + } + void SetAnimatedNodeMeshes(const std::unordered_map &meshes) + { + animatedNodeMeshes = meshes; + } + + std::vector cameras; + std::vector animations; + std::unordered_map animatedNodeTransforms; + std::unordered_map animatedNodeMeshes; // nodeIndex -> meshIndex + + private: + std::string name; + std::vector vertices; + std::vector indices; }; /** * @brief Class for loading and managing 3D models. */ -class ModelLoader { -public: - /** - * @brief Default constructor. - */ - ModelLoader() = default; - // Constructor-based initialization to replace separate Initialize() calls - explicit ModelLoader(Renderer* _renderer) { - if (!Initialize(_renderer)) { - throw std::runtime_error("ModelLoader: initialization failed"); - } - } - - /** - * @brief Destructor for proper cleanup. - */ - ~ModelLoader(); - - /** - * @brief Initialize the model loader. - * @param _renderer Pointer to the renderer. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(Renderer* _renderer); - - /** - * @brief Load a model from a GLTF file. - * @param filename The path to the GLTF file. - * @return Pointer to the loaded model, or nullptr if loading failed. - */ - Model* LoadGLTF(const std::string& filename); - - - /** - * @brief Get a model by name. - * @param name The name of the model. - * @return Pointer to the model, or nullptr if not found. - */ - Model* GetModel(const std::string& name); - - - - /** - * @brief Get extracted lights from a loaded model. - * @param modelName The name of the model. - * @return Vector of extracted lights from the model. - */ - std::vector GetExtractedLights(const std::string& modelName) const; - - /** - * @brief Get material-specific meshes from a loaded model. - * @param modelName The name of the model. - * @return Vector of material meshes from the model. - */ - const std::vector& GetMaterialMeshes(const std::string& modelName) const; - - /** - * @brief Get a material by name. - * @param materialName The name of the material. - * @return Pointer to the material, or nullptr if not found. - */ - Material* GetMaterial(const std::string& materialName) const; - - -private: - // Reference to the renderer - Renderer* renderer = nullptr; - - // Loaded models - std::unordered_map> models; - - // Loaded materials - std::unordered_map> materials; - - // Extracted lights per model - std::unordered_map> extractedLights; - - // Material meshes per model - std::unordered_map> materialMeshes; - - bool hasEmissiveStrengthExtension = false; - - float light_scale = 1.0f; - - /** - * @brief Parse a GLTF file. - * @param filename The path to the GLTF file. - * @param model The model to populate. - * @return True if parsing was successful, false otherwise. - */ - bool ParseGLTF(const std::string& filename, Model* model); - - /** - * @brief Extract lights from GLTF punctual lights extension. - * @param gltfModel The loaded GLTF model. - * @param modelName The name of the model. - * @return True if extraction was successful, false otherwise. - */ - bool ExtractPunctualLights(const class tinygltf::Model& gltfModel, const std::string& modelName); +class ModelLoader +{ + public: + /** + * @brief Default constructor. + */ + ModelLoader() = default; + // Constructor-based initialization to replace separate Initialize() calls + explicit ModelLoader(Renderer *_renderer) + { + if (!Initialize(_renderer)) + { + throw std::runtime_error("ModelLoader: initialization failed"); + } + } + + /** + * @brief Destructor for proper cleanup. + */ + ~ModelLoader(); + + /** + * @brief Initialize the model loader. + * @param _renderer Pointer to the renderer. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(Renderer *_renderer); + + /** + * @brief Load a model from a GLTF file. + * @param filename The path to the GLTF file. + * @return Pointer to the loaded model, or nullptr if loading failed. + */ + Model *LoadGLTF(const std::string &filename); + + /** + * @brief Get a model by name. + * @param name The name of the model. + * @return Pointer to the model, or nullptr if not found. + */ + Model *GetModel(const std::string &name); + + /** + * @brief Get extracted lights from a loaded model. + * @param modelName The name of the model. + * @return Vector of extracted lights from the model. + */ + std::vector GetExtractedLights(const std::string &modelName) const; + + /** + * @brief Get material-specific meshes from a loaded model. + * @param modelName The name of the model. + * @return Vector of material meshes from the model. + */ + const std::vector &GetMaterialMeshes(const std::string &modelName) const; + + /** + * @brief Get a material by name. + * @param materialName The name of the material. + * @return Pointer to the material, or nullptr if not found. + */ + Material *GetMaterial(const std::string &materialName) const; + + /** + * @brief Get animations from a loaded model. + * @param modelName The name of the model. + * @return Vector of animations from the model. + */ + const std::vector &GetAnimations(const std::string &modelName) const; + + private: + // Reference to the renderer + Renderer *renderer = nullptr; + + // Loaded models + std::unordered_map> models; + + // Loaded materials + std::unordered_map> materials; + + // Extracted lights per model + std::unordered_map> extractedLights; + + // Material meshes per model + std::unordered_map> materialMeshes; + + bool hasEmissiveStrengthExtension = false; + + float light_scale = 1.0f; + + /** + * @brief Parse a GLTF file. + * @param filename The path to the GLTF file. + * @param model The model to populate. + * @return True if parsing was successful, false otherwise. + */ + bool ParseGLTF(const std::string &filename, Model *model); + + /** + * @brief Extract lights from GLTF punctual lights extension. + * @param gltfModel The loaded GLTF model. + * @param modelName The name of the model. + * @return True if extraction was successful, false otherwise. + */ + bool ExtractPunctualLights(const class tinygltf::Model &gltfModel, const std::string &modelName); }; diff --git a/attachments/simple_engine/physics_system.cpp b/attachments/simple_engine/physics_system.cpp index dad38dcc..cde0f1d7 100644 --- a/attachments/simple_engine/physics_system.cpp +++ b/attachments/simple_engine/physics_system.cpp @@ -1,1380 +1,1522 @@ +/* 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 "physics_system.h" #include "entity.h" +#include "mesh_component.h" #include "renderer.h" #include "transform_component.h" -#include "mesh_component.h" #include -#include #include +#include #include #include +#include #include -#include - // Concrete implementation of RigidBody -class ConcreteRigidBody final : public RigidBody { -public: - ConcreteRigidBody(Entity* entity, CollisionShape shape, float mass) - : entity(entity), shape(shape), mass(mass) { - // Initialize with the entity's transform if available - if (entity) { - // Get the position, rotation, and scale from the entity's transform component - if (auto* transform = entity->GetComponent()) { - position = transform->GetPosition(); - rotation = glm::quat(transform->GetRotation()); // Convert from Euler angles to quaternion - scale = transform->GetScale(); - } else { - // Fallback to defaults if no transform component - position = glm::vec3(0.0f); - rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion - scale = glm::vec3(1.0f); - } - } - } - - ~ConcreteRigidBody() override = default; - - void SetPosition(const glm::vec3& _position) override { - position = _position; - - // Update entity transform component for visual representation - if (entity) { - if (auto* transform = entity->GetComponent()) { - transform->SetPosition(_position); - } - } - } - - void SetRotation(const glm::quat& _rotation) override { - rotation = _rotation; - - // Update entity transform component for visual representation - if (entity) { - if (auto* transform = entity->GetComponent()) { - // Convert quaternion to Euler angles for the transform component - glm::vec3 eulerAngles = glm::eulerAngles(_rotation); - transform->SetRotation(eulerAngles); - } - } - } - - void SetScale(const glm::vec3& _scale) override { - scale = _scale; - } - - void SetMass(float _mass) override { - mass = _mass; - } - - void SetRestitution(float _restitution) override { - restitution = _restitution; - } - - void SetFriction(float _friction) override { - friction = _friction; - } - - void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition) override { - // In a real implementation, this would apply the force to the rigid body - linearVelocity += force / mass; - } - - void ApplyImpulse(const glm::vec3& impulse, const glm::vec3& localPosition) override { - // In a real implementation, this would apply the impulse to the rigid body - linearVelocity += impulse / mass; - } - - void SetLinearVelocity(const glm::vec3& velocity) override { - linearVelocity = velocity; - } - - void SetAngularVelocity(const glm::vec3& velocity) override { - angularVelocity = velocity; - } - - [[nodiscard]] glm::vec3 GetPosition() const override { - return position; - } - - [[nodiscard]] glm::quat GetRotation() const override { - return rotation; - } - - [[nodiscard]] glm::vec3 GetLinearVelocity() const override { - return linearVelocity; - } - - [[nodiscard]] glm::vec3 GetAngularVelocity() const override { - return angularVelocity; - } - - void SetKinematic(bool _kinematic) override { - // Prevent balls from being set as kinematic - they should always be dynamic - if (entity && entity->GetName().find("Ball_") == 0 && _kinematic) { - return; - } - - kinematic = _kinematic; - } - - [[nodiscard]] bool IsKinematic() const override { - return kinematic; - } - - [[nodiscard]] Entity* GetEntity() const { - return entity; - } - - [[nodiscard]] CollisionShape GetShape() const { - return shape; - } - - [[nodiscard]] float GetMass() const { - return mass; - } - - [[nodiscard]] float GetInverseMass() const { - return mass > 0.0f ? 1.0f / mass : 0.0f; - } - - [[nodiscard]] float GetRestitution() const { - return restitution; - } - - [[nodiscard]] float GetFriction() const { - return friction; - } - - - - -private: - Entity* entity = nullptr; - CollisionShape shape; - - glm::vec3 position = glm::vec3(0.0f); - glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion - glm::vec3 scale = glm::vec3(1.0f); - - glm::vec3 linearVelocity = glm::vec3(0.0f); - glm::vec3 angularVelocity = glm::vec3(0.0f); - - float mass = 1.0f; - float restitution = 0.5f; - float friction = 0.5f; - - bool kinematic = false; - bool markedForRemoval = false; // Flag to mark physics body for removal - - friend class PhysicsSystem; +class ConcreteRigidBody final : public RigidBody +{ + public: + ConcreteRigidBody(Entity *entity, CollisionShape shape, float mass) : + entity(entity), shape(shape), mass(mass) + { + // Initialize with the entity's transform if available + if (entity) + { + // Get the position, rotation, and scale from the entity's transform component + if (auto *transform = entity->GetComponent()) + { + position = transform->GetPosition(); + rotation = glm::quat(transform->GetRotation()); // Convert from Euler angles to quaternion + scale = transform->GetScale(); + } + else + { + // Fallback to defaults if no transform component + position = glm::vec3(0.0f); + rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + scale = glm::vec3(1.0f); + } + } + } + + ~ConcreteRigidBody() override = default; + + void SetPosition(const glm::vec3 &_position) override + { + position = _position; + + // Update entity transform component for visual representation + if (entity) + { + if (auto *transform = entity->GetComponent()) + { + transform->SetPosition(_position); + } + } + } + + void SetRotation(const glm::quat &_rotation) override + { + rotation = _rotation; + + // Update entity transform component for visual representation + if (entity) + { + if (auto *transform = entity->GetComponent()) + { + // Convert quaternion to Euler angles for the transform component + glm::vec3 eulerAngles = glm::eulerAngles(_rotation); + transform->SetRotation(eulerAngles); + } + } + } + + void SetScale(const glm::vec3 &_scale) override + { + scale = _scale; + } + + void SetMass(float _mass) override + { + mass = _mass; + } + + void SetRestitution(float _restitution) override + { + restitution = _restitution; + } + + void SetFriction(float _friction) override + { + friction = _friction; + } + + void ApplyForce(const glm::vec3 &force, const glm::vec3 &localPosition) override + { + // In a real implementation, this would apply the force to the rigid body + linearVelocity += force / mass; + } + + void ApplyImpulse(const glm::vec3 &impulse, const glm::vec3 &localPosition) override + { + // In a real implementation, this would apply the impulse to the rigid body + linearVelocity += impulse / mass; + } + + void SetLinearVelocity(const glm::vec3 &velocity) override + { + linearVelocity = velocity; + } + + void SetAngularVelocity(const glm::vec3 &velocity) override + { + angularVelocity = velocity; + } + + [[nodiscard]] glm::vec3 GetPosition() const override + { + return position; + } + + [[nodiscard]] glm::quat GetRotation() const override + { + return rotation; + } + + [[nodiscard]] glm::vec3 GetLinearVelocity() const override + { + return linearVelocity; + } + + [[nodiscard]] glm::vec3 GetAngularVelocity() const override + { + return angularVelocity; + } + + void SetKinematic(bool _kinematic) override + { + // Prevent balls from being set as kinematic - they should always be dynamic + if (entity && entity->GetName().find("Ball_") == 0 && _kinematic) + { + return; + } + + kinematic = _kinematic; + } + + [[nodiscard]] bool IsKinematic() const override + { + return kinematic; + } + + [[nodiscard]] Entity *GetEntity() const + { + return entity; + } + + [[nodiscard]] CollisionShape GetShape() const + { + return shape; + } + + [[nodiscard]] float GetMass() const + { + return mass; + } + + [[nodiscard]] float GetInverseMass() const + { + return mass > 0.0f ? 1.0f / mass : 0.0f; + } + + [[nodiscard]] float GetRestitution() const + { + return restitution; + } + + [[nodiscard]] float GetFriction() const + { + return friction; + } + + private: + Entity *entity = nullptr; + CollisionShape shape; + + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + glm::vec3 scale = glm::vec3(1.0f); + + glm::vec3 linearVelocity = glm::vec3(0.0f); + glm::vec3 angularVelocity = glm::vec3(0.0f); + + float mass = 1.0f; + float restitution = 0.5f; + float friction = 0.5f; + + bool kinematic = false; + bool markedForRemoval = false; // Flag to mark physics body for removal + + friend class PhysicsSystem; }; -PhysicsSystem::~PhysicsSystem() { - // Destructor implementation - if (initialized && gpuAccelerationEnabled) { - CleanupVulkanResources(); - } - rigidBodies.clear(); +PhysicsSystem::~PhysicsSystem() +{ + // Destructor implementation + if (initialized && gpuAccelerationEnabled) + { + CleanupVulkanResources(); + } + rigidBodies.clear(); } -bool PhysicsSystem::Initialize() { - // Enforce GPU-only physics. If GPU resources cannot be initialized, initialization fails. - - // Renderer must be set for GPU compute physics - if (!renderer) { - std::cerr << "PhysicsSystem::Initialize: Renderer is not set. GPU-only physics cannot proceed." << std::endl; - return false; - } - - // Always keep GPU acceleration enabled (CPU fallback is not allowed) - gpuAccelerationEnabled = true; - - // Initialize Vulkan resources; fail hard if not available - if (!InitializeVulkanResources()) { - std::cerr << "PhysicsSystem::Initialize: Failed to initialize Vulkan resources for physics (GPU-only)." << std::endl; - return false; - } - - initialized = true; - return true; +bool PhysicsSystem::Initialize() +{ + // Enforce GPU-only physics. If GPU resources cannot be initialized, initialization fails. + + // Renderer must be set for GPU compute physics + if (!renderer) + { + std::cerr << "PhysicsSystem::Initialize: Renderer is not set. GPU-only physics cannot proceed." << std::endl; + return false; + } + + // Always keep GPU acceleration enabled (CPU fallback is not allowed) + gpuAccelerationEnabled = true; + + // Initialize Vulkan resources; fail hard if not available + if (!InitializeVulkanResources()) + { + std::cerr << "PhysicsSystem::Initialize: Failed to initialize Vulkan resources for physics (GPU-only)." << std::endl; + return false; + } + + initialized = true; + return true; } -void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) { - // Drain any pending rigid body creations queued from background threads - std::vector toCreate; - { - std::lock_guard lk(pendingMutex); - if (!pendingCreations.empty()) { - toCreate.swap(pendingCreations); - } - } - for (const auto& pc : toCreate) { - if (!pc.entity) continue; - - // Check size limit with proper locking (CreateRigidBody will acquire the lock again, but that's safe) - { - std::lock_guard lock(rigidBodiesMutex); - if (rigidBodies.size() >= maxGPUObjects) break; // avoid oversubscription - } - - RigidBody* rb = CreateRigidBody(pc.entity, pc.shape, pc.mass); - if (rb) { - rb->SetKinematic(pc.kinematic); - rb->SetRestitution(pc.restitution); - rb->SetFriction(pc.friction); - } - } - - // GPU-ONLY physics - NO CPU fallback available - - // Check if GPU physics is properly initialized and available - bool canUseGPUPhysics = false; - { - std::lock_guard lock(rigidBodiesMutex); - canUseGPUPhysics = (rigidBodies.size() <= maxGPUObjects); - } - - if (initialized && gpuAccelerationEnabled && renderer && canUseGPUPhysics) { - // Debug: Log that we're using GPU physics - static bool gpuPhysicsLogged = false; - if (!gpuPhysicsLogged) { - gpuPhysicsLogged = true; - } - SimulatePhysicsOnGPU(deltaTime); - } else { - // NO CPU FALLBACK - GPU physics must work, or physics is disabled - static bool noFallbackLogged = false; - if (!noFallbackLogged) { - noFallbackLogged = true; - } - - } - - - // Clean up rigid bodies marked for removal (happens regardless of GPU/CPU physics path) - CleanupMarkedBodies(); +void PhysicsSystem::Update(std::chrono::milliseconds deltaTime) +{ + // Drain any pending rigid body creations queued from background threads + std::vector toCreate; + { + std::lock_guard lk(pendingMutex); + if (!pendingCreations.empty()) + { + toCreate.swap(pendingCreations); + } + } + for (const auto &pc : toCreate) + { + if (!pc.entity) + continue; + + // Check size limit with proper locking (CreateRigidBody will acquire the lock again, but that's safe) + { + std::lock_guard lock(rigidBodiesMutex); + if (rigidBodies.size() >= maxGPUObjects) + break; // avoid oversubscription + } + + RigidBody *rb = CreateRigidBody(pc.entity, pc.shape, pc.mass); + if (rb) + { + rb->SetKinematic(pc.kinematic); + rb->SetRestitution(pc.restitution); + rb->SetFriction(pc.friction); + } + } + + // GPU-ONLY physics - NO CPU fallback available + + // Check if GPU physics is properly initialized and available + bool canUseGPUPhysics = false; + { + std::lock_guard lock(rigidBodiesMutex); + canUseGPUPhysics = (rigidBodies.size() <= maxGPUObjects); + } + + if (initialized && gpuAccelerationEnabled && renderer && canUseGPUPhysics) + { + // Debug: Log that we're using GPU physics + static bool gpuPhysicsLogged = false; + if (!gpuPhysicsLogged) + { + gpuPhysicsLogged = true; + } + SimulatePhysicsOnGPU(deltaTime); + } + else + { + // NO CPU FALLBACK - GPU physics must work, or physics is disabled + static bool noFallbackLogged = false; + if (!noFallbackLogged) + { + noFallbackLogged = true; + } + } + + // Clean up rigid bodies marked for removal (happens regardless of GPU/CPU physics path) + CleanupMarkedBodies(); } -void PhysicsSystem::EnqueueRigidBodyCreation(Entity* entity, - CollisionShape shape, - float mass, - bool kinematic, - float restitution, - float friction) { - if (!entity) return; - std::lock_guard lk(pendingMutex); - pendingCreations.push_back(PendingCreation{entity, shape, mass, kinematic, restitution, friction}); +void PhysicsSystem::EnqueueRigidBodyCreation(Entity *entity, + CollisionShape shape, + float mass, + bool kinematic, + float restitution, + float friction) +{ + if (!entity) + return; + std::lock_guard lk(pendingMutex); + pendingCreations.push_back(PendingCreation{entity, shape, mass, kinematic, restitution, friction}); } -RigidBody* PhysicsSystem::CreateRigidBody(Entity* entity, CollisionShape shape, float mass) { - // Create a new rigid body - auto rigidBody = std::make_unique(entity, shape, mass); +RigidBody *PhysicsSystem::CreateRigidBody(Entity *entity, CollisionShape shape, float mass) +{ + // Create a new rigid body + auto rigidBody = std::make_unique(entity, shape, mass); - // Store the rigid body with thread-safe access - std::lock_guard lock(rigidBodiesMutex); - rigidBodies.push_back(std::move(rigidBody)); + // Store the rigid body with thread-safe access + std::lock_guard lock(rigidBodiesMutex); + rigidBodies.push_back(std::move(rigidBody)); - return rigidBodies.back().get(); + return rigidBodies.back().get(); } -bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { - std::lock_guard lock(rigidBodiesMutex); - - // Find the rigid body in the vector - auto it = std::ranges::find_if(rigidBodies, - [rigidBody](const std::unique_ptr& rb) { - return rb.get() == rigidBody; - }); +bool PhysicsSystem::RemoveRigidBody(RigidBody *rigidBody) +{ + std::lock_guard lock(rigidBodiesMutex); + + // Find the rigid body in the vector + auto it = std::ranges::find_if(rigidBodies, + [rigidBody](const std::unique_ptr &rb) { + return rb.get() == rigidBody; + }); - if (it != rigidBodies.end()) { - // Remove the rigid body - rigidBodies.erase(it); + if (it != rigidBodies.end()) + { + // Remove the rigid body + rigidBodies.erase(it); - return true; - } + return true; + } - std::cerr << "PhysicsSystem::RemoveRigidBody: Rigid body not found" << std::endl; - return false; + std::cerr << "PhysicsSystem::RemoveRigidBody: Rigid body not found" << std::endl; + return false; } -void PhysicsSystem::SetGravity(const glm::vec3& _gravity) { - gravity = _gravity; +void PhysicsSystem::SetGravity(const glm::vec3 &_gravity) +{ + gravity = _gravity; } -glm::vec3 PhysicsSystem::GetGravity() const { - return gravity; +glm::vec3 PhysicsSystem::GetGravity() const +{ + return gravity; } -bool PhysicsSystem::Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, - glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) const { - // Normalize the direction vector - glm::vec3 normalizedDirection = glm::normalize(direction); - - // Variables to track the closest hit - float closestHitDistance = maxDistance; - bool hitFound = false; - glm::vec3 closestHitPosition; - glm::vec3 closestHitNormal; - Entity* closestHitEntity = nullptr; - - // Protect access to rigidBodies vector during iteration - std::lock_guard lock(rigidBodiesMutex); - - // Check each rigid body for intersection - for (const auto& rigidBody : rigidBodies) { - auto concreteRigidBody = dynamic_cast(rigidBody.get()); - Entity* entity = concreteRigidBody->GetEntity(); - - // Skip if the entity is null - if (!entity) { - continue; - } - - // Get the position and shape of the rigid body - glm::vec3 position = concreteRigidBody->GetPosition(); - CollisionShape shape = concreteRigidBody->GetShape(); - - // Variables for hit detection - float hitDistance = 0.0f; - glm::vec3 localHitPosition; - glm::vec3 localHitNormal; - bool hit = false; - - // Check for intersection based on the shape - switch (shape) { - case CollisionShape::Sphere: { - // Sphere intersection test - float radius = 0.0335f; // Tennis ball radius to match actual ball - - // Calculate coefficients for quadratic equation - glm::vec3 oc = origin - position; - float a = glm::dot(normalizedDirection, normalizedDirection); - float b = 2.0f * glm::dot(oc, normalizedDirection); - float c = glm::dot(oc, oc) - radius * radius; - float discriminant = b * b - 4 * a * c; - - if (discriminant >= 0) { - // Calculate intersection distance - float t = (-b - std::sqrt(discriminant)) / (2.0f * a); - - // Check if the intersection is within range - if (t > 0 && t < closestHitDistance) { - hitDistance = t; - localHitPosition = origin + normalizedDirection * t; - localHitNormal = glm::normalize(localHitPosition - position); - hit = true; - } - } - break; - } - case CollisionShape::Box: { - // Box intersection test (AABB) - glm::vec3 halfExtents(0.5f, 0.5f, 0.5f); // Default box size - - // Calculate min and max bounds of the box - glm::vec3 boxMin = position - halfExtents; - glm::vec3 boxMax = position + halfExtents; - - // Calculate intersection with each slab - float tmin = -INFINITY, tmax = INFINITY; - - for (int i = 0; i < 3; i++) { - if (std::abs(normalizedDirection[i]) < 0.0001f) { - // Ray is parallel to the slab, check if origin is within slab - if (origin[i] < boxMin[i] || origin[i] > boxMax[i]) { - // No intersection - hit = false; - break; - } - } else { - // Calculate intersection distances - float ood = 1.0f / normalizedDirection[i]; - float t1 = (boxMin[i] - origin[i]) * ood; - float t2 = (boxMax[i] - origin[i]) * ood; - - // Ensure t1 <= t2 - if (t1 > t2) { - std::swap(t1, t2); - } - - // Update tmin and tmax - tmin = std::max(tmin, t1); - tmax = std::min(tmax, t2); - - if (tmin > tmax) { - // No intersection - hit = false; - break; - } - } - } - - // Check if the intersection is within range - if (tmin > 0 && tmin < closestHitDistance) { - hitDistance = tmin; - localHitPosition = origin + normalizedDirection * tmin; - - // Calculate normal based on which face was hit - glm::vec3 center = position; - glm::vec3 d = localHitPosition - center; - float bias = 1.00001f; // Small bias to ensure we get the correct face - - localHitNormal = glm::vec3(0.0f); - if (d.x > halfExtents.x * bias) localHitNormal = glm::vec3(1, 0, 0); - else if (d.x < -halfExtents.x * bias) localHitNormal = glm::vec3(-1, 0, 0); - else if (d.y > halfExtents.y * bias) localHitNormal = glm::vec3(0, 1, 0); - else if (d.y < -halfExtents.y * bias) localHitNormal = glm::vec3(0, -1, 0); - else if (d.z > halfExtents.z * bias) localHitNormal = glm::vec3(0, 0, 1); - else if (d.z < -halfExtents.z * bias) localHitNormal = glm::vec3(0, 0, -1); - - hit = true; - } - break; - } - case CollisionShape::Capsule: { - // Capsule intersection test - // Simplified as a line segment with spheres at each end - float radius = 0.5f; // Default radius - float halfHeight = 0.5f; // Default half-height - - // Define capsule line segment - glm::vec3 capsuleA = position + glm::vec3(0, -halfHeight, 0); - glm::vec3 capsuleB = position + glm::vec3(0, halfHeight, 0); - - // Calculate the closest point on a line segment - glm::vec3 ab = capsuleB - capsuleA; - glm::vec3 ao = origin - capsuleA; - - float t = glm::dot(ao, ab) / glm::dot(ab, ab); - t = glm::clamp(t, 0.0f, 1.0f); - - glm::vec3 closestPoint = capsuleA + ab * t; - - // Sphere intersection test with the closest point - glm::vec3 oc = origin - closestPoint; - float a = glm::dot(normalizedDirection, normalizedDirection); - float b = 2.0f * glm::dot(oc, normalizedDirection); - float c = glm::dot(oc, oc) - radius * radius; - - if (float discriminant = b * b - 4 * a * c; discriminant >= 0) { - // Calculate intersection distance - - // Check if the intersection is within range - if (float id = (-b - std::sqrt(discriminant)) / (2.0f * a); id > 0 && id < closestHitDistance) { - hitDistance = id; - localHitPosition = origin + normalizedDirection * id; - localHitNormal = glm::normalize(localHitPosition - closestPoint); - hit = true; - } - } - break; - } - case CollisionShape::Mesh: { - // Proper mesh intersection test using triangle data - if (auto* meshComponent = entity->GetComponent()) { - const auto& vertices = meshComponent->GetVertices(); - const auto& indices = meshComponent->GetIndices(); - - // Test intersection with each triangle in the mesh - for (size_t i = 0; i < indices.size(); i += 3) { - if (i + 2 >= indices.size()) break; - - // Get triangle vertices - glm::vec3 v0 = vertices[indices[i]].position; - glm::vec3 v1 = vertices[indices[i + 1]].position; - glm::vec3 v2 = vertices[indices[i + 2]].position; - - // Transform vertices to world space - if (auto* transform = entity->GetComponent()) { - glm::mat4 transformMatrix = transform->GetModelMatrix(); - v0 = glm::vec3(transformMatrix * glm::vec4(v0, 1.0f)); - v1 = glm::vec3(transformMatrix * glm::vec4(v1, 1.0f)); - v2 = glm::vec3(transformMatrix * glm::vec4(v2, 1.0f)); - } - - // Ray-triangle intersection using Möller-Trumbore algorithm - glm::vec3 edge1 = v1 - v0; - glm::vec3 edge2 = v2 - v0; - glm::vec3 h = glm::cross(normalizedDirection, edge2); - float a = glm::dot(edge1, h); - - if (a > -0.00001f && a < 0.00001f) continue; // Ray parallel to triangle - - float f = 1.0f / a; - glm::vec3 s = origin - v0; - float u = f * glm::dot(s, h); - - if (u < 0.0f || u > 1.0f) continue; - - glm::vec3 q = glm::cross(s, edge1); - float v = f * glm::dot(normalizedDirection, q); - - if (v < 0.0f || u + v > 1.0f) continue; - - float t = f * glm::dot(edge2, q); - - if (t > 0.00001f && t < closestHitDistance) { - hitDistance = t; - localHitPosition = origin + normalizedDirection * t; - localHitNormal = glm::normalize(glm::cross(edge1, edge2)); - hit = true; - closestHitDistance = t; // Update for closer triangles - } - } - } - break; - } - default: - break; - } - - // Update the closest hit if a hit was found - if (hit && hitDistance < closestHitDistance) { - closestHitDistance = hitDistance; - closestHitPosition = localHitPosition; - closestHitNormal = localHitNormal; - closestHitEntity = entity; - hitFound = true; - } - } - - // Set output parameters if a hit was found - if (hitFound) { - if (hitPosition) { - *hitPosition = closestHitPosition; - } - - if (hitNormal) { - *hitNormal = closestHitNormal; - } - - if (hitEntity) { - *hitEntity = closestHitEntity; - } - } - - return hitFound; +bool PhysicsSystem::Raycast(const glm::vec3 &origin, const glm::vec3 &direction, float maxDistance, + glm::vec3 *hitPosition, glm::vec3 *hitNormal, Entity **hitEntity) const +{ + // Normalize the direction vector + glm::vec3 normalizedDirection = glm::normalize(direction); + + // Variables to track the closest hit + float closestHitDistance = maxDistance; + bool hitFound = false; + glm::vec3 closestHitPosition; + glm::vec3 closestHitNormal; + Entity *closestHitEntity = nullptr; + + // Protect access to rigidBodies vector during iteration + std::lock_guard lock(rigidBodiesMutex); + + // Check each rigid body for intersection + for (const auto &rigidBody : rigidBodies) + { + auto concreteRigidBody = dynamic_cast(rigidBody.get()); + Entity *entity = concreteRigidBody->GetEntity(); + + // Skip if the entity is null + if (!entity) + { + continue; + } + + // Get the position and shape of the rigid body + glm::vec3 position = concreteRigidBody->GetPosition(); + CollisionShape shape = concreteRigidBody->GetShape(); + + // Variables for hit detection + float hitDistance = 0.0f; + glm::vec3 localHitPosition; + glm::vec3 localHitNormal; + bool hit = false; + + // Check for intersection based on the shape + switch (shape) + { + case CollisionShape::Sphere: + { + // Sphere intersection test + float radius = 0.0335f; // Tennis ball radius to match actual ball + + // Calculate coefficients for quadratic equation + glm::vec3 oc = origin - position; + float a = glm::dot(normalizedDirection, normalizedDirection); + float b = 2.0f * glm::dot(oc, normalizedDirection); + float c = glm::dot(oc, oc) - radius * radius; + float discriminant = b * b - 4 * a * c; + + if (discriminant >= 0) + { + // Calculate intersection distance + float t = (-b - std::sqrt(discriminant)) / (2.0f * a); + + // Check if the intersection is within range + if (t > 0 && t < closestHitDistance) + { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(localHitPosition - position); + hit = true; + } + } + break; + } + case CollisionShape::Box: + { + // Box intersection test (AABB) + glm::vec3 halfExtents(0.5f, 0.5f, 0.5f); // Default box size + + // Calculate min and max bounds of the box + glm::vec3 boxMin = position - halfExtents; + glm::vec3 boxMax = position + halfExtents; + + // Calculate intersection with each slab + float tmin = -INFINITY, tmax = INFINITY; + + for (int i = 0; i < 3; i++) + { + if (std::abs(normalizedDirection[i]) < 0.0001f) + { + // Ray is parallel to the slab, check if origin is within slab + if (origin[i] < boxMin[i] || origin[i] > boxMax[i]) + { + // No intersection + hit = false; + break; + } + } + else + { + // Calculate intersection distances + float ood = 1.0f / normalizedDirection[i]; + float t1 = (boxMin[i] - origin[i]) * ood; + float t2 = (boxMax[i] - origin[i]) * ood; + + // Ensure t1 <= t2 + if (t1 > t2) + { + std::swap(t1, t2); + } + + // Update tmin and tmax + tmin = std::max(tmin, t1); + tmax = std::min(tmax, t2); + + if (tmin > tmax) + { + // No intersection + hit = false; + break; + } + } + } + + // Check if the intersection is within range + if (tmin > 0 && tmin < closestHitDistance) + { + hitDistance = tmin; + localHitPosition = origin + normalizedDirection * tmin; + + // Calculate normal based on which face was hit + glm::vec3 center = position; + glm::vec3 d = localHitPosition - center; + float bias = 1.00001f; // Small bias to ensure we get the correct face + + localHitNormal = glm::vec3(0.0f); + if (d.x > halfExtents.x * bias) + localHitNormal = glm::vec3(1, 0, 0); + else if (d.x < -halfExtents.x * bias) + localHitNormal = glm::vec3(-1, 0, 0); + else if (d.y > halfExtents.y * bias) + localHitNormal = glm::vec3(0, 1, 0); + else if (d.y < -halfExtents.y * bias) + localHitNormal = glm::vec3(0, -1, 0); + else if (d.z > halfExtents.z * bias) + localHitNormal = glm::vec3(0, 0, 1); + else if (d.z < -halfExtents.z * bias) + localHitNormal = glm::vec3(0, 0, -1); + + hit = true; + } + break; + } + case CollisionShape::Capsule: + { + // Capsule intersection test + // Simplified as a line segment with spheres at each end + float radius = 0.5f; // Default radius + float halfHeight = 0.5f; // Default half-height + + // Define capsule line segment + glm::vec3 capsuleA = position + glm::vec3(0, -halfHeight, 0); + glm::vec3 capsuleB = position + glm::vec3(0, halfHeight, 0); + + // Calculate the closest point on a line segment + glm::vec3 ab = capsuleB - capsuleA; + glm::vec3 ao = origin - capsuleA; + + float t = glm::dot(ao, ab) / glm::dot(ab, ab); + t = glm::clamp(t, 0.0f, 1.0f); + + glm::vec3 closestPoint = capsuleA + ab * t; + + // Sphere intersection test with the closest point + glm::vec3 oc = origin - closestPoint; + float a = glm::dot(normalizedDirection, normalizedDirection); + float b = 2.0f * glm::dot(oc, normalizedDirection); + float c = glm::dot(oc, oc) - radius * radius; + + if (float discriminant = b * b - 4 * a * c; discriminant >= 0) + { + // Calculate intersection distance + + // Check if the intersection is within range + if (float id = (-b - std::sqrt(discriminant)) / (2.0f * a); id > 0 && id < closestHitDistance) + { + hitDistance = id; + localHitPosition = origin + normalizedDirection * id; + localHitNormal = glm::normalize(localHitPosition - closestPoint); + hit = true; + } + } + break; + } + case CollisionShape::Mesh: + { + // Proper mesh intersection test using triangle data + if (auto *meshComponent = entity->GetComponent()) + { + const auto &vertices = meshComponent->GetVertices(); + const auto &indices = meshComponent->GetIndices(); + + // Test intersection with each triangle in the mesh + for (size_t i = 0; i < indices.size(); i += 3) + { + if (i + 2 >= indices.size()) + break; + + // Get triangle vertices + glm::vec3 v0 = vertices[indices[i]].position; + glm::vec3 v1 = vertices[indices[i + 1]].position; + glm::vec3 v2 = vertices[indices[i + 2]].position; + + // Transform vertices to world space + if (auto *transform = entity->GetComponent()) + { + glm::mat4 transformMatrix = transform->GetModelMatrix(); + v0 = glm::vec3(transformMatrix * glm::vec4(v0, 1.0f)); + v1 = glm::vec3(transformMatrix * glm::vec4(v1, 1.0f)); + v2 = glm::vec3(transformMatrix * glm::vec4(v2, 1.0f)); + } + + // Ray-triangle intersection using Möller-Trumbore algorithm + glm::vec3 edge1 = v1 - v0; + glm::vec3 edge2 = v2 - v0; + glm::vec3 h = glm::cross(normalizedDirection, edge2); + float a = glm::dot(edge1, h); + + if (a > -0.00001f && a < 0.00001f) + continue; // Ray parallel to triangle + + float f = 1.0f / a; + glm::vec3 s = origin - v0; + float u = f * glm::dot(s, h); + + if (u < 0.0f || u > 1.0f) + continue; + + glm::vec3 q = glm::cross(s, edge1); + float v = f * glm::dot(normalizedDirection, q); + + if (v < 0.0f || u + v > 1.0f) + continue; + + float t = f * glm::dot(edge2, q); + + if (t > 0.00001f && t < closestHitDistance) + { + hitDistance = t; + localHitPosition = origin + normalizedDirection * t; + localHitNormal = glm::normalize(glm::cross(edge1, edge2)); + hit = true; + closestHitDistance = t; // Update for closer triangles + } + } + } + break; + } + default: + break; + } + + // Update the closest hit if a hit was found + if (hit && hitDistance < closestHitDistance) + { + closestHitDistance = hitDistance; + closestHitPosition = localHitPosition; + closestHitNormal = localHitNormal; + closestHitEntity = entity; + hitFound = true; + } + } + + // Set output parameters if a hit was found + if (hitFound) + { + if (hitPosition) + { + *hitPosition = closestHitPosition; + } + + if (hitNormal) + { + *hitNormal = closestHitNormal; + } + + if (hitEntity) + { + *hitEntity = closestHitEntity; + } + } + + return hitFound; } // Helper function to read a shader file -static std::vector readFile(const std::string& filename) { - std::ifstream file(filename, std::ios::ate | std::ios::binary); - if (!file.is_open()) { - throw std::runtime_error("Failed to open file: " + filename); - } - - size_t fileSize = file.tellg(); - std::vector buffer(fileSize); - - file.seekg(0); - file.read(buffer.data(), static_cast(fileSize)); - file.close(); - - return buffer; +static std::vector readFile(const std::string &filename) +{ + std::ifstream file(filename, std::ios::ate | std::ios::binary); + if (!file.is_open()) + { + throw std::runtime_error("Failed to open file: " + filename); + } + + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), static_cast(fileSize)); + file.close(); + + return buffer; } // Helper function to create a shader module -static vk::raii::ShaderModule createShaderModule(const vk::raii::Device& device, const std::vector& code) { - vk::ShaderModuleCreateInfo createInfo; - createInfo.codeSize = code.size(); - createInfo.pCode = reinterpret_cast(code.data()); +static vk::raii::ShaderModule createShaderModule(const vk::raii::Device &device, const std::vector &code) +{ + vk::ShaderModuleCreateInfo createInfo; + createInfo.codeSize = code.size(); + createInfo.pCode = reinterpret_cast(code.data()); - return {device, createInfo}; + return {device, createInfo}; } -bool PhysicsSystem::InitializeVulkanResources() { - if (!renderer) { - std::cerr << "Renderer is not set" << std::endl; - return false; - } - - vk::Device device = renderer->GetDevice(); - if (!device) { - std::cerr << "Vulkan device is not valid" << std::endl; - return false; - } - - try { - // Create shader modules - const vk::raii::Device& raiiDevice = renderer->GetRaiiDevice(); - - std::vector integrateShaderCode = readFile("shaders/physics.spv"); - vulkanResources.integrateShaderModule = createShaderModule(raiiDevice, integrateShaderCode); - - std::vector broadPhaseShaderCode = readFile("shaders/physics.spv"); - vulkanResources.broadPhaseShaderModule = createShaderModule(raiiDevice, broadPhaseShaderCode); - - std::vector narrowPhaseShaderCode = readFile("shaders/physics.spv"); - vulkanResources.narrowPhaseShaderModule = createShaderModule(raiiDevice, narrowPhaseShaderCode); - - std::vector resolveShaderCode = readFile("shaders/physics.spv"); - vulkanResources.resolveShaderModule = createShaderModule(raiiDevice, resolveShaderCode); - - // Create a descriptor set layout - std::array bindings = { - // Physics data buffer - vk::DescriptorSetLayoutBinding( - 0, // binding - vk::DescriptorType::eStorageBuffer, // descriptorType - 1, // descriptorCount - vk::ShaderStageFlagBits::eCompute, // stageFlags - nullptr // pImmutableSamplers - ), - // Collision data buffer - vk::DescriptorSetLayoutBinding( - 1, // binding - vk::DescriptorType::eStorageBuffer, // descriptorType - 1, // descriptorCount - vk::ShaderStageFlagBits::eCompute, // stageFlags - nullptr // pImmutableSamplers - ), - // Pair buffer - vk::DescriptorSetLayoutBinding( - 2, // binding - vk::DescriptorType::eStorageBuffer, // descriptorType - 1, // descriptorCount - vk::ShaderStageFlagBits::eCompute, // stageFlags - nullptr // pImmutableSamplers - ), - // Counter buffer - vk::DescriptorSetLayoutBinding( - 3, // binding - vk::DescriptorType::eStorageBuffer, // descriptorType - 1, // descriptorCount - vk::ShaderStageFlagBits::eCompute, // stageFlags - nullptr // pImmutableSamplers - ), - // Parameters buffer - vk::DescriptorSetLayoutBinding( - 4, // binding - vk::DescriptorType::eUniformBuffer, // descriptorType - 1, // descriptorCount - vk::ShaderStageFlagBits::eCompute, // stageFlags - nullptr // pImmutableSamplers - ) - }; - - vk::DescriptorSetLayoutCreateInfo layoutInfo; - layoutInfo.bindingCount = static_cast(bindings.size()); - layoutInfo.pBindings = bindings.data(); - vulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(raiiDevice, layoutInfo); - - // Create pipeline layout - vk::PipelineLayoutCreateInfo pipelineLayoutInfo; - pipelineLayoutInfo.setLayoutCount = 1; - vk::DescriptorSetLayout descriptorSetLayout = *vulkanResources.descriptorSetLayout; - pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; - vulkanResources.pipelineLayout = vk::raii::PipelineLayout(raiiDevice, pipelineLayoutInfo); - - // Create compute pipelines - vk::ComputePipelineCreateInfo pipelineInfo; - pipelineInfo.layout = *vulkanResources.pipelineLayout; - pipelineInfo.basePipelineHandle = nullptr; - - // Integrate pipeline - vk::PipelineShaderStageCreateInfo integrateStageInfo; - integrateStageInfo.stage = vk::ShaderStageFlagBits::eCompute; - integrateStageInfo.module = *vulkanResources.integrateShaderModule; - integrateStageInfo.pName = "IntegrateCS"; - pipelineInfo.stage = integrateStageInfo; - vulkanResources.integratePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - - // Broad phase pipeline - vk::PipelineShaderStageCreateInfo broadPhaseStageInfo; - broadPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; - broadPhaseStageInfo.module = *vulkanResources.broadPhaseShaderModule; - broadPhaseStageInfo.pName = "BroadPhaseCS"; - pipelineInfo.stage = broadPhaseStageInfo; - vulkanResources.broadPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - - // Narrow phase pipeline - vk::PipelineShaderStageCreateInfo narrowPhaseStageInfo; - narrowPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; - narrowPhaseStageInfo.module = *vulkanResources.narrowPhaseShaderModule; - narrowPhaseStageInfo.pName = "NarrowPhaseCS"; - pipelineInfo.stage = narrowPhaseStageInfo; - vulkanResources.narrowPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - - // Resolve pipeline - vk::PipelineShaderStageCreateInfo resolveStageInfo; - resolveStageInfo.stage = vk::ShaderStageFlagBits::eCompute; - resolveStageInfo.module = *vulkanResources.resolveShaderModule; - resolveStageInfo.pName = "ResolveCS"; - pipelineInfo.stage = resolveStageInfo; - vulkanResources.resolvePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); - - // Create buffers - vk::DeviceSize physicsBufferSize = sizeof(GPUPhysicsData) * maxGPUObjects; - vk::DeviceSize collisionBufferSize = sizeof(GPUCollisionData) * maxGPUCollisions; - vk::DeviceSize pairBufferSize = sizeof(uint32_t) * 2 * maxGPUCollisions; - vk::DeviceSize counterBufferSize = sizeof(uint32_t) * 2; - vk::DeviceSize paramsBufferSize = ((sizeof(PhysicsParams) + 63) / 64) * 64; - - // Create a physics buffer - vk::BufferCreateInfo bufferInfo; - bufferInfo.size = physicsBufferSize; - bufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; - bufferInfo.sharingMode = vk::SharingMode::eExclusive; - - try { - vulkanResources.physicsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); - - vk::MemoryRequirements memRequirements = vulkanResources.physicsBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo allocInfo; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = renderer->FindMemoryType( - memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - vulkanResources.physicsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); - vulkanResources.physicsBuffer.bindMemory(*vulkanResources.physicsBufferMemory, 0); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create physics buffer: " + std::string(e.what())); - } - - // Create a collision buffer - bufferInfo.size = collisionBufferSize; - try { - vulkanResources.collisionBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); - - vk::MemoryRequirements memRequirements = vulkanResources.collisionBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo allocInfo; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = renderer->FindMemoryType( - memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - vulkanResources.collisionBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); - vulkanResources.collisionBuffer.bindMemory(*vulkanResources.collisionBufferMemory, 0); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create collision buffer: " + std::string(e.what())); - } - - // Create a pair buffer - bufferInfo.size = pairBufferSize; - try { - vulkanResources.pairBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); - - vk::MemoryRequirements memRequirements = vulkanResources.pairBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo allocInfo; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = renderer->FindMemoryType( - memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - vulkanResources.pairBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); - vulkanResources.pairBuffer.bindMemory(*vulkanResources.pairBufferMemory, 0); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create pair buffer: " + std::string(e.what())); - } - - // Create the counter-buffer - bufferInfo.size = counterBufferSize; - try { - vulkanResources.counterBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); - - vk::MemoryRequirements memRequirements = vulkanResources.counterBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo allocInfo; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = renderer->FindMemoryType( - memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - vulkanResources.counterBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); - vulkanResources.counterBuffer.bindMemory(*vulkanResources.counterBufferMemory, 0); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create counter buffer: " + std::string(e.what())); - } - - // Create a params buffer - bufferInfo.size = paramsBufferSize; - bufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; - try { - vulkanResources.paramsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); - - vk::MemoryRequirements memRequirements = vulkanResources.paramsBuffer.getMemoryRequirements(); - - vk::MemoryAllocateInfo allocInfo; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = renderer->FindMemoryType( - memRequirements.memoryTypeBits, - vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent - ); - - vulkanResources.paramsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); - vulkanResources.paramsBuffer.bindMemory(*vulkanResources.paramsBufferMemory, 0); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create params buffer: " + std::string(e.what())); - } - - // Create persistent mapped memory pointers for improved performance - try { - // Map entire memory objects persistently to satisfy VK_WHOLE_SIZE flush alignment requirements - vulkanResources.persistentPhysicsMemory = vulkanResources.physicsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); - vulkanResources.persistentCounterMemory = vulkanResources.counterBufferMemory.mapMemory(0, VK_WHOLE_SIZE); - vulkanResources.persistentParamsMemory = vulkanResources.paramsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to create persistent mapped memory: " + std::string(e.what())); - } - - // Initialize counter-buffer using persistent memory - uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count - memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); - - // Create a descriptor pool with capacity for 4 physics stages - std::array poolSizes = { - vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 16), // 4 storage buffers × 4 stages - vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 4) // 1 uniform buffer × 4 stages - }; - - vk::DescriptorPoolCreateInfo poolInfo; - poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; - poolInfo.poolSizeCount = static_cast(poolSizes.size()); - poolInfo.pPoolSizes = poolSizes.data(); - poolInfo.maxSets = 4; // Support 4 descriptor sets for 4 physics stages - vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); - - // Allocate descriptor sets - vk::DescriptorSetAllocateInfo descriptorSetAllocInfo; - descriptorSetAllocInfo.descriptorPool = *vulkanResources.descriptorPool; - descriptorSetAllocInfo.descriptorSetCount = 1; - vk::DescriptorSetLayout descriptorSetLayoutRef = *vulkanResources.descriptorSetLayout; - descriptorSetAllocInfo.pSetLayouts = &descriptorSetLayoutRef; - - try { - vulkanResources.descriptorSets = raiiDevice.allocateDescriptorSets(descriptorSetAllocInfo); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to allocate descriptor sets: " + std::string(e.what())); - } - - // Update descriptor sets - vk::DescriptorBufferInfo physicsBufferInfo; - physicsBufferInfo.buffer = *vulkanResources.physicsBuffer; - physicsBufferInfo.offset = 0; - physicsBufferInfo.range = physicsBufferSize; - - vk::DescriptorBufferInfo collisionBufferInfo; - collisionBufferInfo.buffer = *vulkanResources.collisionBuffer; - collisionBufferInfo.offset = 0; - collisionBufferInfo.range = collisionBufferSize; - - vk::DescriptorBufferInfo pairBufferInfo; - pairBufferInfo.buffer = *vulkanResources.pairBuffer; - pairBufferInfo.offset = 0; - pairBufferInfo.range = pairBufferSize; - - vk::DescriptorBufferInfo counterBufferInfo; - counterBufferInfo.buffer = *vulkanResources.counterBuffer; - counterBufferInfo.offset = 0; - counterBufferInfo.range = counterBufferSize; - - vk::DescriptorBufferInfo paramsBufferInfo; - paramsBufferInfo.buffer = *vulkanResources.paramsBuffer; - paramsBufferInfo.offset = 0; - paramsBufferInfo.range = VK_WHOLE_SIZE; // Use VK_WHOLE_SIZE to ensure the entire buffer is accessible - - std::array descriptorWrites; - - // Physics buffer - descriptorWrites[0].setDstSet(*vulkanResources.descriptorSets[0]) - .setDstBinding(0) - .setDstArrayElement(0) - .setDescriptorCount(1) - .setDescriptorType(vk::DescriptorType::eStorageBuffer) - .setPBufferInfo(&physicsBufferInfo); - - // Collision buffer - descriptorWrites[1].setDstSet(*vulkanResources.descriptorSets[0]) - .setDstBinding(1) - .setDstArrayElement(0) - .setDescriptorCount(1) - .setDescriptorType(vk::DescriptorType::eStorageBuffer) - .setPBufferInfo(&collisionBufferInfo); - - // Pair buffer - descriptorWrites[2].setDstSet(*vulkanResources.descriptorSets[0]) - .setDstBinding(2) - .setDstArrayElement(0) - .setDescriptorCount(1) - .setDescriptorType(vk::DescriptorType::eStorageBuffer) - .setPBufferInfo(&pairBufferInfo); - - // Counter buffer - descriptorWrites[3].setDstSet(*vulkanResources.descriptorSets[0]) - .setDstBinding(3) - .setDstArrayElement(0) - .setDescriptorCount(1) - .setDescriptorType(vk::DescriptorType::eStorageBuffer) - .setPBufferInfo(&counterBufferInfo); - - // Params buffer - descriptorWrites[4].setDstSet(*vulkanResources.descriptorSets[0]) - .setDstBinding(4) - .setDstArrayElement(0) - .setDescriptorCount(1) - .setDescriptorType(vk::DescriptorType::eUniformBuffer) - .setPBufferInfo(¶msBufferInfo); - - raiiDevice.updateDescriptorSets(descriptorWrites, nullptr); - - // Create a command pool bound to the compute queue family used by the renderer - vk::CommandPoolCreateInfo commandPoolInfo; - commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; - commandPoolInfo.queueFamilyIndex = renderer->GetComputeQueueFamilyIndex(); - vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); - - // Allocate command buffer - vk::CommandBufferAllocateInfo commandBufferInfo; - commandBufferInfo.commandPool = *vulkanResources.commandPool; - commandBufferInfo.level = vk::CommandBufferLevel::ePrimary; - commandBufferInfo.commandBufferCount = 1; - - try { - std::vector commandBuffers = raiiDevice.allocateCommandBuffers(commandBufferInfo); - vulkanResources.commandBuffer = std::move(commandBuffers.front()); - } catch (const std::exception& e) { - throw std::runtime_error("Failed to allocate command buffer: " + std::string(e.what())); - } - - // Create a dedicated fence for compute synchronization - vk::FenceCreateInfo fenceInfo{}; - vulkanResources.computeFence = vk::raii::Fence(raiiDevice, fenceInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Error initializing Vulkan resources: " << e.what() << std::endl; - CleanupVulkanResources(); - return false; - } +bool PhysicsSystem::InitializeVulkanResources() +{ + if (!renderer) + { + std::cerr << "Renderer is not set" << std::endl; + return false; + } + + vk::Device device = renderer->GetDevice(); + if (!device) + { + std::cerr << "Vulkan device is not valid" << std::endl; + return false; + } + + try + { + // Create shader modules + const vk::raii::Device &raiiDevice = renderer->GetRaiiDevice(); + + std::vector integrateShaderCode = readFile("shaders/physics.spv"); + vulkanResources.integrateShaderModule = createShaderModule(raiiDevice, integrateShaderCode); + + std::vector broadPhaseShaderCode = readFile("shaders/physics.spv"); + vulkanResources.broadPhaseShaderModule = createShaderModule(raiiDevice, broadPhaseShaderCode); + + std::vector narrowPhaseShaderCode = readFile("shaders/physics.spv"); + vulkanResources.narrowPhaseShaderModule = createShaderModule(raiiDevice, narrowPhaseShaderCode); + + std::vector resolveShaderCode = readFile("shaders/physics.spv"); + vulkanResources.resolveShaderModule = createShaderModule(raiiDevice, resolveShaderCode); + + // Create a descriptor set layout + std::array bindings = { + // Physics data buffer + vk::DescriptorSetLayoutBinding( + 0, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Collision data buffer + vk::DescriptorSetLayoutBinding( + 1, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Pair buffer + vk::DescriptorSetLayoutBinding( + 2, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Counter buffer + vk::DescriptorSetLayoutBinding( + 3, // binding + vk::DescriptorType::eStorageBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + ), + // Parameters buffer + vk::DescriptorSetLayoutBinding( + 4, // binding + vk::DescriptorType::eUniformBuffer, // descriptorType + 1, // descriptorCount + vk::ShaderStageFlagBits::eCompute, // stageFlags + nullptr // pImmutableSamplers + )}; + + vk::DescriptorSetLayoutCreateInfo layoutInfo; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + vulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(raiiDevice, layoutInfo); + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setLayoutCount = 1; + vk::DescriptorSetLayout descriptorSetLayout = *vulkanResources.descriptorSetLayout; + pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; + vulkanResources.pipelineLayout = vk::raii::PipelineLayout(raiiDevice, pipelineLayoutInfo); + + // Create compute pipelines + vk::ComputePipelineCreateInfo pipelineInfo; + pipelineInfo.layout = *vulkanResources.pipelineLayout; + pipelineInfo.basePipelineHandle = nullptr; + + // Integrate pipeline + vk::PipelineShaderStageCreateInfo integrateStageInfo; + integrateStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + integrateStageInfo.module = *vulkanResources.integrateShaderModule; + integrateStageInfo.pName = "IntegrateCS"; + pipelineInfo.stage = integrateStageInfo; + vulkanResources.integratePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + + // Broad phase pipeline + vk::PipelineShaderStageCreateInfo broadPhaseStageInfo; + broadPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + broadPhaseStageInfo.module = *vulkanResources.broadPhaseShaderModule; + broadPhaseStageInfo.pName = "BroadPhaseCS"; + pipelineInfo.stage = broadPhaseStageInfo; + vulkanResources.broadPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + + // Narrow phase pipeline + vk::PipelineShaderStageCreateInfo narrowPhaseStageInfo; + narrowPhaseStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + narrowPhaseStageInfo.module = *vulkanResources.narrowPhaseShaderModule; + narrowPhaseStageInfo.pName = "NarrowPhaseCS"; + pipelineInfo.stage = narrowPhaseStageInfo; + vulkanResources.narrowPhasePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + + // Resolve pipeline + vk::PipelineShaderStageCreateInfo resolveStageInfo; + resolveStageInfo.stage = vk::ShaderStageFlagBits::eCompute; + resolveStageInfo.module = *vulkanResources.resolveShaderModule; + resolveStageInfo.pName = "ResolveCS"; + pipelineInfo.stage = resolveStageInfo; + vulkanResources.resolvePipeline = vk::raii::Pipeline(raiiDevice, nullptr, pipelineInfo); + + // Create buffers + vk::DeviceSize physicsBufferSize = sizeof(GPUPhysicsData) * maxGPUObjects; + vk::DeviceSize collisionBufferSize = sizeof(GPUCollisionData) * maxGPUCollisions; + vk::DeviceSize pairBufferSize = sizeof(uint32_t) * 2 * maxGPUCollisions; + vk::DeviceSize counterBufferSize = sizeof(uint32_t) * 2; + vk::DeviceSize paramsBufferSize = ((sizeof(PhysicsParams) + 63) / 64) * 64; + + // Create a physics buffer + vk::BufferCreateInfo bufferInfo; + bufferInfo.size = physicsBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eStorageBuffer; + bufferInfo.sharingMode = vk::SharingMode::eExclusive; + + try + { + vulkanResources.physicsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.physicsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vulkanResources.physicsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.physicsBuffer.bindMemory(*vulkanResources.physicsBufferMemory, 0); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create physics buffer: " + std::string(e.what())); + } + + // Create a collision buffer + bufferInfo.size = collisionBufferSize; + try + { + vulkanResources.collisionBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.collisionBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vulkanResources.collisionBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.collisionBuffer.bindMemory(*vulkanResources.collisionBufferMemory, 0); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create collision buffer: " + std::string(e.what())); + } + + // Create a pair buffer + bufferInfo.size = pairBufferSize; + try + { + vulkanResources.pairBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.pairBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vulkanResources.pairBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.pairBuffer.bindMemory(*vulkanResources.pairBufferMemory, 0); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create pair buffer: " + std::string(e.what())); + } + + // Create the counter-buffer + bufferInfo.size = counterBufferSize; + try + { + vulkanResources.counterBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.counterBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vulkanResources.counterBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.counterBuffer.bindMemory(*vulkanResources.counterBufferMemory, 0); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create counter buffer: " + std::string(e.what())); + } + + // Create a params buffer + bufferInfo.size = paramsBufferSize; + bufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer; + try + { + vulkanResources.paramsBuffer = vk::raii::Buffer(raiiDevice, bufferInfo); + + vk::MemoryRequirements memRequirements = vulkanResources.paramsBuffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = renderer->FindMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + vulkanResources.paramsBufferMemory = vk::raii::DeviceMemory(raiiDevice, allocInfo); + vulkanResources.paramsBuffer.bindMemory(*vulkanResources.paramsBufferMemory, 0); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create params buffer: " + std::string(e.what())); + } + + // Create persistent mapped memory pointers for improved performance + try + { + // Map entire memory objects persistently to satisfy VK_WHOLE_SIZE flush alignment requirements + vulkanResources.persistentPhysicsMemory = vulkanResources.physicsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); + vulkanResources.persistentCounterMemory = vulkanResources.counterBufferMemory.mapMemory(0, VK_WHOLE_SIZE); + vulkanResources.persistentParamsMemory = vulkanResources.paramsBufferMemory.mapMemory(0, VK_WHOLE_SIZE); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to create persistent mapped memory: " + std::string(e.what())); + } + + // Initialize counter-buffer using persistent memory + uint32_t initialCounters[2] = {0, 0}; // [0] = pair count, [1] = collision count + memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); + + // Create a descriptor pool with capacity for 4 physics stages + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 16), // 4 storage buffers × 4 stages + vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 4) // 1 uniform buffer × 4 stages + }; + + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + poolInfo.poolSizeCount = static_cast(poolSizes.size()); + poolInfo.pPoolSizes = poolSizes.data(); + poolInfo.maxSets = 4; // Support 4 descriptor sets for 4 physics stages + vulkanResources.descriptorPool = vk::raii::DescriptorPool(raiiDevice, poolInfo); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocInfo; + descriptorSetAllocInfo.descriptorPool = *vulkanResources.descriptorPool; + descriptorSetAllocInfo.descriptorSetCount = 1; + vk::DescriptorSetLayout descriptorSetLayoutRef = *vulkanResources.descriptorSetLayout; + descriptorSetAllocInfo.pSetLayouts = &descriptorSetLayoutRef; + + try + { + vulkanResources.descriptorSets = raiiDevice.allocateDescriptorSets(descriptorSetAllocInfo); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to allocate descriptor sets: " + std::string(e.what())); + } + + // Update descriptor sets + vk::DescriptorBufferInfo physicsBufferInfo; + physicsBufferInfo.buffer = *vulkanResources.physicsBuffer; + physicsBufferInfo.offset = 0; + physicsBufferInfo.range = physicsBufferSize; + + vk::DescriptorBufferInfo collisionBufferInfo; + collisionBufferInfo.buffer = *vulkanResources.collisionBuffer; + collisionBufferInfo.offset = 0; + collisionBufferInfo.range = collisionBufferSize; + + vk::DescriptorBufferInfo pairBufferInfo; + pairBufferInfo.buffer = *vulkanResources.pairBuffer; + pairBufferInfo.offset = 0; + pairBufferInfo.range = pairBufferSize; + + vk::DescriptorBufferInfo counterBufferInfo; + counterBufferInfo.buffer = *vulkanResources.counterBuffer; + counterBufferInfo.offset = 0; + counterBufferInfo.range = counterBufferSize; + + vk::DescriptorBufferInfo paramsBufferInfo; + paramsBufferInfo.buffer = *vulkanResources.paramsBuffer; + paramsBufferInfo.offset = 0; + paramsBufferInfo.range = VK_WHOLE_SIZE; // Use VK_WHOLE_SIZE to ensure the entire buffer is accessible + + std::array descriptorWrites; + + // Physics buffer + descriptorWrites[0].setDstSet(*vulkanResources.descriptorSets[0]).setDstBinding(0).setDstArrayElement(0).setDescriptorCount(1).setDescriptorType(vk::DescriptorType::eStorageBuffer).setPBufferInfo(&physicsBufferInfo); + + // Collision buffer + descriptorWrites[1].setDstSet(*vulkanResources.descriptorSets[0]).setDstBinding(1).setDstArrayElement(0).setDescriptorCount(1).setDescriptorType(vk::DescriptorType::eStorageBuffer).setPBufferInfo(&collisionBufferInfo); + + // Pair buffer + descriptorWrites[2].setDstSet(*vulkanResources.descriptorSets[0]).setDstBinding(2).setDstArrayElement(0).setDescriptorCount(1).setDescriptorType(vk::DescriptorType::eStorageBuffer).setPBufferInfo(&pairBufferInfo); + + // Counter buffer + descriptorWrites[3].setDstSet(*vulkanResources.descriptorSets[0]).setDstBinding(3).setDstArrayElement(0).setDescriptorCount(1).setDescriptorType(vk::DescriptorType::eStorageBuffer).setPBufferInfo(&counterBufferInfo); + + // Params buffer + descriptorWrites[4].setDstSet(*vulkanResources.descriptorSets[0]).setDstBinding(4).setDstArrayElement(0).setDescriptorCount(1).setDescriptorType(vk::DescriptorType::eUniformBuffer).setPBufferInfo(¶msBufferInfo); + + raiiDevice.updateDescriptorSets(descriptorWrites, nullptr); + + // Create a command pool bound to the compute queue family used by the renderer + vk::CommandPoolCreateInfo commandPoolInfo; + commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; + commandPoolInfo.queueFamilyIndex = renderer->GetComputeQueueFamilyIndex(); + vulkanResources.commandPool = vk::raii::CommandPool(raiiDevice, commandPoolInfo); + + // Allocate command buffer + vk::CommandBufferAllocateInfo commandBufferInfo; + commandBufferInfo.commandPool = *vulkanResources.commandPool; + commandBufferInfo.level = vk::CommandBufferLevel::ePrimary; + commandBufferInfo.commandBufferCount = 1; + + try + { + std::vector commandBuffers = raiiDevice.allocateCommandBuffers(commandBufferInfo); + vulkanResources.commandBuffer = std::move(commandBuffers.front()); + } + catch (const std::exception &e) + { + throw std::runtime_error("Failed to allocate command buffer: " + std::string(e.what())); + } + + // Create a dedicated fence for compute synchronization + vk::FenceCreateInfo fenceInfo{}; + vulkanResources.computeFence = vk::raii::Fence(raiiDevice, fenceInfo); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Error initializing Vulkan resources: " << e.what() << std::endl; + CleanupVulkanResources(); + return false; + } } -void PhysicsSystem::CleanupVulkanResources() { - if (!renderer) { - return; - } - - // Wait for the device to be idle before cleaning up - renderer->WaitIdle(); - - // Cleanup in proper order to avoid validation errors - // 1. Clear descriptor sets BEFORE destroying the descriptor pool - vulkanResources.descriptorSets.clear(); - - // 2. Destroy pipelines before pipeline layout - vulkanResources.resolvePipeline = nullptr; - vulkanResources.narrowPhasePipeline = nullptr; - vulkanResources.broadPhasePipeline = nullptr; - vulkanResources.integratePipeline = nullptr; - - // 3. Destroy pipeline layout before descriptor set layout - vulkanResources.pipelineLayout = nullptr; - vulkanResources.descriptorSetLayout = nullptr; - - // 4. Destroy shader modules - vulkanResources.resolveShaderModule = nullptr; - vulkanResources.narrowPhaseShaderModule = nullptr; - vulkanResources.broadPhaseShaderModule = nullptr; - vulkanResources.integrateShaderModule = nullptr; - - // 5. Destroy the descriptor pool after descriptor sets are cleared - vulkanResources.descriptorPool = nullptr; - - // 6. Destroy the command buffer before the command pool - vulkanResources.commandBuffer = nullptr; - vulkanResources.commandPool = nullptr; - - // 7. Destroy compute fence - vulkanResources.computeFence = nullptr; - - // 8. Unmap persistent memory pointers before destroying buffer memory - if (vulkanResources.persistentPhysicsMemory && *vulkanResources.physicsBufferMemory) { - vulkanResources.physicsBufferMemory.unmapMemory(); - vulkanResources.persistentPhysicsMemory = nullptr; - } - - if (vulkanResources.persistentCounterMemory && *vulkanResources.counterBufferMemory) { - vulkanResources.counterBufferMemory.unmapMemory(); - vulkanResources.persistentCounterMemory = nullptr; - } - - if (vulkanResources.persistentParamsMemory && *vulkanResources.paramsBufferMemory) { - vulkanResources.paramsBufferMemory.unmapMemory(); - vulkanResources.persistentParamsMemory = nullptr; - } - - // 8. Destroy buffers and their memory - vulkanResources.paramsBuffer = nullptr; - vulkanResources.paramsBufferMemory = nullptr; - vulkanResources.counterBuffer = nullptr; - vulkanResources.counterBufferMemory = nullptr; - vulkanResources.pairBuffer = nullptr; - vulkanResources.pairBufferMemory = nullptr; - vulkanResources.collisionBuffer = nullptr; - vulkanResources.collisionBufferMemory = nullptr; - vulkanResources.physicsBuffer = nullptr; - vulkanResources.physicsBufferMemory = nullptr; +void PhysicsSystem::CleanupVulkanResources() +{ + if (!renderer) + { + return; + } + + // Wait for the device to be idle before cleaning up + renderer->WaitIdle(); + + // Cleanup in proper order to avoid validation errors + // 1. Clear descriptor sets BEFORE destroying the descriptor pool + vulkanResources.descriptorSets.clear(); + + // 2. Destroy pipelines before pipeline layout + vulkanResources.resolvePipeline = nullptr; + vulkanResources.narrowPhasePipeline = nullptr; + vulkanResources.broadPhasePipeline = nullptr; + vulkanResources.integratePipeline = nullptr; + + // 3. Destroy pipeline layout before descriptor set layout + vulkanResources.pipelineLayout = nullptr; + vulkanResources.descriptorSetLayout = nullptr; + + // 4. Destroy shader modules + vulkanResources.resolveShaderModule = nullptr; + vulkanResources.narrowPhaseShaderModule = nullptr; + vulkanResources.broadPhaseShaderModule = nullptr; + vulkanResources.integrateShaderModule = nullptr; + + // 5. Destroy the descriptor pool after descriptor sets are cleared + vulkanResources.descriptorPool = nullptr; + + // 6. Destroy the command buffer before the command pool + vulkanResources.commandBuffer = nullptr; + vulkanResources.commandPool = nullptr; + + // 7. Destroy compute fence + vulkanResources.computeFence = nullptr; + + // 8. Unmap persistent memory pointers before destroying buffer memory + if (vulkanResources.persistentPhysicsMemory && *vulkanResources.physicsBufferMemory) + { + vulkanResources.physicsBufferMemory.unmapMemory(); + vulkanResources.persistentPhysicsMemory = nullptr; + } + + if (vulkanResources.persistentCounterMemory && *vulkanResources.counterBufferMemory) + { + vulkanResources.counterBufferMemory.unmapMemory(); + vulkanResources.persistentCounterMemory = nullptr; + } + + if (vulkanResources.persistentParamsMemory && *vulkanResources.paramsBufferMemory) + { + vulkanResources.paramsBufferMemory.unmapMemory(); + vulkanResources.persistentParamsMemory = nullptr; + } + + // 8. Destroy buffers and their memory + vulkanResources.paramsBuffer = nullptr; + vulkanResources.paramsBufferMemory = nullptr; + vulkanResources.counterBuffer = nullptr; + vulkanResources.counterBufferMemory = nullptr; + vulkanResources.pairBuffer = nullptr; + vulkanResources.pairBufferMemory = nullptr; + vulkanResources.collisionBuffer = nullptr; + vulkanResources.collisionBufferMemory = nullptr; + vulkanResources.physicsBuffer = nullptr; + vulkanResources.physicsBufferMemory = nullptr; } -void PhysicsSystem::UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const { - if (!renderer) { - return; - } - - // Validate Vulkan resources and persistent memory pointers before using them - if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || - *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.counterBufferMemory == VK_NULL_HANDLE || - *vulkanResources.paramsBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBufferMemory == VK_NULL_HANDLE || - !vulkanResources.persistentPhysicsMemory || !vulkanResources.persistentCounterMemory || !vulkanResources.persistentParamsMemory) { - std::cerr << "PhysicsSystem::UpdateGPUPhysicsData: Invalid Vulkan resources or persistent memory pointers" << std::endl; - return; - } - - // Skip physics buffer operations if no rigid bodies exist - if (!rigidBodies.empty()) { - // Use persistent mapped memory for physics buffer - auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); - const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); - for (size_t i = 0; i < count; i++) { - const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); - if (!concreteRigidBody) { continue; } - - gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); - gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, - concreteRigidBody->GetRotation().z, concreteRigidBody->GetRotation().w); - gpuData[i].linearVelocity = glm::vec4(concreteRigidBody->GetLinearVelocity(), concreteRigidBody->GetRestitution()); - gpuData[i].angularVelocity = glm::vec4(concreteRigidBody->GetAngularVelocity(), concreteRigidBody->GetFriction()); - // CRITICAL FIX: Initialize forces properly instead of always resetting to zero - // For balls, we want to start with zero force and let the shader apply gravity - // For static geometry, forces should remain zero - auto initialForce = glm::vec3(0.0f); - auto initialTorque = glm::vec3(0.0f); - - // For dynamic bodies (balls), allow forces to be applied by - // The shader will add gravity and other forces each frame - bool isKinematic = concreteRigidBody->IsKinematic(); - gpuData[i].force = glm::vec4(initialForce, isKinematic ? 1.0f : 0.0f); - // Use gravity only for dynamic bodies - gpuData[i].torque = glm::vec4(initialTorque, isKinematic ? 0.0f : 1.0f); - - // Set collider data based on a collider type - switch (concreteRigidBody->GetShape()) { - case CollisionShape::Sphere: - // Use tennis ball radius (0.0335f) instead of hardcoded 0.5f - gpuData[i].colliderData = glm::vec4(0.0335f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; - case CollisionShape::Box: - gpuData[i].colliderData = glm::vec4(0.5f, 0.5f, 0.5f, static_cast(1)); // 1 = Box - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; - case CollisionShape::Mesh: - { - // Compute an axis-aligned bounding box from the entity's mesh in WORLD space - // and pass half-extents and local offset to the GPU. This enables sphere-geometry - // collisions against actual imported GLTF geometry rather than a constant box. - glm::vec3 halfExtents(5.0f); - glm::vec3 localOffset(0.0f); - - if (auto* entity = concreteRigidBody->GetEntity()) { - auto* meshComp = entity->GetComponent(); - auto* xform = entity->GetComponent(); - if (meshComp && xform && meshComp->HasLocalAABB()) { - glm::vec3 localMin = meshComp->GetLocalAABBMin(); - glm::vec3 localMax = meshComp->GetLocalAABBMax(); - glm::vec3 localCenter = 0.5f * (localMin + localMax); - glm::vec3 localHalfExtents = 0.5f * (localMax - localMin); - - glm::mat4 model = (meshComp->GetInstanceCount() > 0) - ? meshComp->GetInstance(0).getModelMatrix() - : xform->GetModelMatrix(); - glm::vec3 centerWS = glm::vec3(model * glm::vec4(localCenter, 1.0f)); - - glm::mat3 RS = glm::mat3(model); - glm::mat3 absRS; - absRS[0] = glm::abs(RS[0]); - absRS[1] = glm::abs(RS[1]); - absRS[2] = glm::abs(RS[2]); - - glm::vec3 worldHalfExtents = absRS * localHalfExtents; - halfExtents = glm::max(worldHalfExtents, glm::vec3(0.01f)); - - // Offset relative to rigid body position - localOffset = centerWS - concreteRigidBody->GetPosition(); - } - } - - // Encode Mesh collider as Mesh (type=2) for GPU narrowphase handling (sphere vs mesh) - gpuData[i].colliderData = glm::vec4(halfExtents, static_cast(2)); // 2 = Mesh (represented as world AABB) - gpuData[i].colliderData2 = glm::vec4(localOffset, 0.0f); - } - break; - default: - gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid - gpuData[i].colliderData2 = glm::vec4(0.0f); - break; - } - } - } - - // Reset counters using persistent mapped memory - uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count - memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); - - // Update params buffer - PhysicsParams params{}; - params.deltaTime = deltaTime.count() * 0.001f; // Use actual deltaTime instead of fixed timestep - params.numBodies = static_cast(std::min(rigidBodies.size(), static_cast(maxGPUObjects))); - params.maxCollisions = maxGPUCollisions; - params.padding = 0.0f; // Initialize padding to zero for proper std140 alignment - params.gravity = glm::vec4(gravity, 0.0f); // Pack gravity into vec4 with padding - - // Update params buffer using persistent mapped memory - memcpy(vulkanResources.persistentParamsMemory, ¶ms, sizeof(PhysicsParams)); - - // CRITICAL FIX: Explicit memory flush to ensure HOST_COHERENT memory is fully visible to GPU - // Even with HOST_COHERENT flag, some systems may have cache coherency issues with partial writes - // Use VK_WHOLE_SIZE to avoid nonCoherentAtomSize alignment validation errors - try { - const vk::raii::Device& device = renderer->GetRaiiDevice(); - // Flush params buffer - vk::MappedMemoryRange flushRangeParams; - flushRangeParams.memory = *vulkanResources.paramsBufferMemory; - flushRangeParams.offset = 0; - flushRangeParams.size = VK_WHOLE_SIZE; - device.flushMappedMemoryRanges(flushRangeParams); - // Flush physics buffer (object data) - vk::MappedMemoryRange flushRangePhysics; - flushRangePhysics.memory = *vulkanResources.physicsBufferMemory; - flushRangePhysics.offset = 0; - flushRangePhysics.size = VK_WHOLE_SIZE; - device.flushMappedMemoryRanges(flushRangePhysics); - // Flush counter buffer (pair and collision counters) - vk::MappedMemoryRange flushRangeCounter; - flushRangeCounter.memory = *vulkanResources.counterBufferMemory; - flushRangeCounter.offset = 0; - flushRangeCounter.size = VK_WHOLE_SIZE; - device.flushMappedMemoryRanges(flushRangeCounter); - } catch (const std::exception& e) { - fprintf(stderr, "WARNING: Failed to flush mapped physics memory: %s", e.what()); - } +void PhysicsSystem::UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const +{ + if (!renderer) + { + return; + } + + // Validate Vulkan resources and persistent memory pointers before using them + if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || + *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.counterBufferMemory == VK_NULL_HANDLE || + *vulkanResources.paramsBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBufferMemory == VK_NULL_HANDLE || + !vulkanResources.persistentPhysicsMemory || !vulkanResources.persistentCounterMemory || !vulkanResources.persistentParamsMemory) + { + std::cerr << "PhysicsSystem::UpdateGPUPhysicsData: Invalid Vulkan resources or persistent memory pointers" << std::endl; + return; + } + + // Skip physics buffer operations if no rigid bodies exist + if (!rigidBodies.empty()) + { + // Use persistent mapped memory for physics buffer + auto *gpuData = static_cast(vulkanResources.persistentPhysicsMemory); + const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); + for (size_t i = 0; i < count; i++) + { + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + if (!concreteRigidBody) + { + continue; + } + + gpuData[i].position = glm::vec4(concreteRigidBody->GetPosition(), concreteRigidBody->GetInverseMass()); + gpuData[i].rotation = glm::vec4(concreteRigidBody->GetRotation().x, concreteRigidBody->GetRotation().y, + concreteRigidBody->GetRotation().z, concreteRigidBody->GetRotation().w); + gpuData[i].linearVelocity = glm::vec4(concreteRigidBody->GetLinearVelocity(), concreteRigidBody->GetRestitution()); + gpuData[i].angularVelocity = glm::vec4(concreteRigidBody->GetAngularVelocity(), concreteRigidBody->GetFriction()); + // CRITICAL FIX: Initialize forces properly instead of always resetting to zero + // For balls, we want to start with zero force and let the shader apply gravity + // For static geometry, forces should remain zero + auto initialForce = glm::vec3(0.0f); + auto initialTorque = glm::vec3(0.0f); + + // For dynamic bodies (balls), allow forces to be applied by + // The shader will add gravity and other forces each frame + bool isKinematic = concreteRigidBody->IsKinematic(); + gpuData[i].force = glm::vec4(initialForce, isKinematic ? 1.0f : 0.0f); + // Use gravity only for dynamic bodies + gpuData[i].torque = glm::vec4(initialTorque, isKinematic ? 0.0f : 1.0f); + + // Set collider data based on a collider type + switch (concreteRigidBody->GetShape()) + { + case CollisionShape::Sphere: + // Use tennis ball radius (0.0335f) instead of hardcoded 0.5f + gpuData[i].colliderData = glm::vec4(0.0335f, 0.0f, 0.0f, static_cast(0)); // 0 = Sphere + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + case CollisionShape::Box: + gpuData[i].colliderData = glm::vec4(0.5f, 0.5f, 0.5f, static_cast(1)); // 1 = Box + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + case CollisionShape::Mesh: + { + // Compute an axis-aligned bounding box from the entity's mesh in WORLD space + // and pass half-extents and local offset to the GPU. This enables sphere-geometry + // collisions against actual imported GLTF geometry rather than a constant box. + glm::vec3 halfExtents(5.0f); + glm::vec3 localOffset(0.0f); + + if (auto *entity = concreteRigidBody->GetEntity()) + { + auto *meshComp = entity->GetComponent(); + auto *xform = entity->GetComponent(); + if (meshComp && xform && meshComp->HasLocalAABB()) + { + glm::vec3 localMin = meshComp->GetLocalAABBMin(); + glm::vec3 localMax = meshComp->GetLocalAABBMax(); + glm::vec3 localCenter = 0.5f * (localMin + localMax); + glm::vec3 localHalfExtents = 0.5f * (localMax - localMin); + + glm::mat4 model = (meshComp->GetInstanceCount() > 0) ? meshComp->GetInstance(0).getModelMatrix() : xform->GetModelMatrix(); + glm::vec3 centerWS = glm::vec3(model * glm::vec4(localCenter, 1.0f)); + + glm::mat3 RS = glm::mat3(model); + glm::mat3 absRS; + absRS[0] = glm::abs(RS[0]); + absRS[1] = glm::abs(RS[1]); + absRS[2] = glm::abs(RS[2]); + + glm::vec3 worldHalfExtents = absRS * localHalfExtents; + halfExtents = glm::max(worldHalfExtents, glm::vec3(0.01f)); + + // Offset relative to rigid body position + localOffset = centerWS - concreteRigidBody->GetPosition(); + } + } + + // Encode Mesh collider as Mesh (type=2) for GPU narrowphase handling (sphere vs mesh) + gpuData[i].colliderData = glm::vec4(halfExtents, static_cast(2)); // 2 = Mesh (represented as world AABB) + gpuData[i].colliderData2 = glm::vec4(localOffset, 0.0f); + } + break; + default: + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); // Invalid + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + } + } + } + + // Reset counters using persistent mapped memory + uint32_t initialCounters[2] = {0, 0}; // [0] = pair count, [1] = collision count + memcpy(vulkanResources.persistentCounterMemory, initialCounters, sizeof(initialCounters)); + + // Update params buffer + PhysicsParams params{}; + params.deltaTime = deltaTime.count() * 0.001f; // Use actual deltaTime instead of fixed timestep + params.numBodies = static_cast(std::min(rigidBodies.size(), static_cast(maxGPUObjects))); + params.maxCollisions = maxGPUCollisions; + params.padding = 0.0f; // Initialize padding to zero for proper std140 alignment + params.gravity = glm::vec4(gravity, 0.0f); // Pack gravity into vec4 with padding + + // Update params buffer using persistent mapped memory + memcpy(vulkanResources.persistentParamsMemory, ¶ms, sizeof(PhysicsParams)); + + // CRITICAL FIX: Explicit memory flush to ensure HOST_COHERENT memory is fully visible to GPU + // Even with HOST_COHERENT flag, some systems may have cache coherency issues with partial writes + // Use VK_WHOLE_SIZE to avoid nonCoherentAtomSize alignment validation errors + try + { + const vk::raii::Device &device = renderer->GetRaiiDevice(); + // Flush params buffer + vk::MappedMemoryRange flushRangeParams; + flushRangeParams.memory = *vulkanResources.paramsBufferMemory; + flushRangeParams.offset = 0; + flushRangeParams.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangeParams); + // Flush physics buffer (object data) + vk::MappedMemoryRange flushRangePhysics; + flushRangePhysics.memory = *vulkanResources.physicsBufferMemory; + flushRangePhysics.offset = 0; + flushRangePhysics.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangePhysics); + // Flush counter buffer (pair and collision counters) + vk::MappedMemoryRange flushRangeCounter; + flushRangeCounter.memory = *vulkanResources.counterBufferMemory; + flushRangeCounter.offset = 0; + flushRangeCounter.size = VK_WHOLE_SIZE; + device.flushMappedMemoryRanges(flushRangeCounter); + } + catch (const std::exception &e) + { + fprintf(stderr, "WARNING: Failed to flush mapped physics memory: %s", e.what()); + } } -void PhysicsSystem::ReadbackGPUPhysicsData() const { - if (!renderer) { - return; - } - - // Validate Vulkan resources and persistent memory pointers before using them - if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || - !vulkanResources.persistentPhysicsMemory) { - return; - } - - // Wait for a dedicated compute fence to ensure GPU compute operations are complete before reading back data - const vk::raii::Device& device = renderer->GetRaiiDevice(); - vk::Result result = device.waitForFences(*vulkanResources.computeFence, VK_TRUE, UINT64_MAX); - if (result != vk::Result::eSuccess) { - return; - } - - // Ensure GPU writes to HOST_VISIBLE memory are visible to the host before reading - try { - vk::MappedMemoryRange invalidateRangePhysics; - invalidateRangePhysics.memory = *vulkanResources.physicsBufferMemory; - invalidateRangePhysics.offset = 0; - invalidateRangePhysics.size = VK_WHOLE_SIZE; - - vk::MappedMemoryRange invalidateRangeCounter; - invalidateRangeCounter.memory = *vulkanResources.counterBufferMemory; - invalidateRangeCounter.offset = 0; - invalidateRangeCounter.size = VK_WHOLE_SIZE; - - device.invalidateMappedMemoryRanges({invalidateRangePhysics, invalidateRangeCounter}); - } catch (const std::exception&) { - // On HOST_COHERENT heaps this may not be required; ignore errors - } - - // Optional debug: read and log pair/collision counters for a few frames - if (vulkanResources.persistentCounterMemory) { - static uint32_t lastPairCount = UINT32_MAX; - static uint32_t lastCollisionCount = UINT32_MAX; - const uint32_t* counters = static_cast(vulkanResources.persistentCounterMemory); - uint32_t pairCount = counters[0]; - uint32_t collisionCount = counters[1]; - if (pairCount != lastPairCount || collisionCount != lastCollisionCount) { - // std::cout << "Physics GPU counters - pairs: " << pairCount << ", collisions: " << collisionCount << std::endl; - lastPairCount = pairCount; - lastCollisionCount = collisionCount; - } - } - - // Skip physics buffer operations if no rigid bodies exist - if (!rigidBodies.empty()) { - // Use persistent mapped memory for physics buffer readback - const auto* gpuData = static_cast(vulkanResources.persistentPhysicsMemory); - const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); - for (size_t i = 0; i < count; i++) { - const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); - if (!concreteRigidBody) { continue; } - - // Skip kinematic bodies - if (concreteRigidBody->IsKinematic()) { - continue; - } - - auto newPosition = glm::vec3(gpuData[i].position); - auto newVelocity = glm::vec3(gpuData[i].linearVelocity); - - concreteRigidBody->SetPosition(newPosition); - concreteRigidBody->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, - gpuData[i].rotation.y, gpuData[i].rotation.z)); - concreteRigidBody->SetLinearVelocity(newVelocity); - concreteRigidBody->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); - } - } +void PhysicsSystem::ReadbackGPUPhysicsData() const +{ + if (!renderer) + { + return; + } + + // Validate Vulkan resources and persistent memory pointers before using them + if (*vulkanResources.physicsBuffer == VK_NULL_HANDLE || *vulkanResources.physicsBufferMemory == VK_NULL_HANDLE || + !vulkanResources.persistentPhysicsMemory) + { + return; + } + + // Wait for a dedicated compute fence to ensure GPU compute operations are complete before reading back data + const vk::raii::Device &device = renderer->GetRaiiDevice(); + vk::Result result = device.waitForFences(*vulkanResources.computeFence, VK_TRUE, UINT64_MAX); + if (result != vk::Result::eSuccess) + { + return; + } + + // Ensure GPU writes to HOST_VISIBLE memory are visible to the host before reading + try + { + vk::MappedMemoryRange invalidateRangePhysics; + invalidateRangePhysics.memory = *vulkanResources.physicsBufferMemory; + invalidateRangePhysics.offset = 0; + invalidateRangePhysics.size = VK_WHOLE_SIZE; + + vk::MappedMemoryRange invalidateRangeCounter; + invalidateRangeCounter.memory = *vulkanResources.counterBufferMemory; + invalidateRangeCounter.offset = 0; + invalidateRangeCounter.size = VK_WHOLE_SIZE; + + device.invalidateMappedMemoryRanges({invalidateRangePhysics, invalidateRangeCounter}); + } + catch (const std::exception &) + { + // On HOST_COHERENT heaps this may not be required; ignore errors + } + + // Optional debug: read and log pair/collision counters for a few frames + if (vulkanResources.persistentCounterMemory) + { + static uint32_t lastPairCount = UINT32_MAX; + static uint32_t lastCollisionCount = UINT32_MAX; + const uint32_t *counters = static_cast(vulkanResources.persistentCounterMemory); + uint32_t pairCount = counters[0]; + uint32_t collisionCount = counters[1]; + if (pairCount != lastPairCount || collisionCount != lastCollisionCount) + { + // std::cout << "Physics GPU counters - pairs: " << pairCount << ", collisions: " << collisionCount << std::endl; + lastPairCount = pairCount; + lastCollisionCount = collisionCount; + } + } + + // Skip physics buffer operations if no rigid bodies exist + if (!rigidBodies.empty()) + { + // Use persistent mapped memory for physics buffer readback + const auto *gpuData = static_cast(vulkanResources.persistentPhysicsMemory); + const size_t count = std::min(rigidBodies.size(), static_cast(maxGPUObjects)); + for (size_t i = 0; i < count; i++) + { + const auto concreteRigidBody = dynamic_cast(rigidBodies[i].get()); + if (!concreteRigidBody) + { + continue; + } + + // Skip kinematic bodies + if (concreteRigidBody->IsKinematic()) + { + continue; + } + + auto newPosition = glm::vec3(gpuData[i].position); + auto newVelocity = glm::vec3(gpuData[i].linearVelocity); + + concreteRigidBody->SetPosition(newPosition); + concreteRigidBody->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, + gpuData[i].rotation.y, gpuData[i].rotation.z)); + concreteRigidBody->SetLinearVelocity(newVelocity); + concreteRigidBody->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); + } + } } -void PhysicsSystem::SimulatePhysicsOnGPU(const std::chrono::milliseconds deltaTime) const { - if (!renderer) { - fprintf(stderr, "SimulatePhysicsOnGPU: No renderer available"); - return; - } - - // Validate Vulkan resources before using them - if (*vulkanResources.broadPhasePipeline == VK_NULL_HANDLE || *vulkanResources.narrowPhasePipeline == VK_NULL_HANDLE || - *vulkanResources.integratePipeline == VK_NULL_HANDLE || *vulkanResources.pipelineLayout == VK_NULL_HANDLE || - vulkanResources.descriptorSets.empty() || *vulkanResources.physicsBuffer == VK_NULL_HANDLE || - *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBuffer == VK_NULL_HANDLE) { - return; - } - - // Update physics data on the GPU - UpdateGPUPhysicsData(deltaTime); - - // Reset the command buffer before beginning (required for reuse) - vulkanResources.commandBuffer.reset(); - - // Begin command buffer - vk::CommandBufferBeginInfo beginInfo; - beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; - - vulkanResources.commandBuffer.begin(beginInfo); - - vulkanResources.commandBuffer.bindDescriptorSets( - vk::PipelineBindPoint::eCompute, - *vulkanResources.pipelineLayout, - 0, - **vulkanResources.descriptorSets.data(), - nullptr - ); - - // Add a memory barrier to ensure all host-written buffer data (uniform + storage) is visible to compute shaders - // We use ShaderRead | ShaderWrite since compute will read and write storage buffers - vk::MemoryBarrier hostToDeviceBarrier; - hostToDeviceBarrier.srcAccessMask = vk::AccessFlagBits::eHostWrite; - hostToDeviceBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; - - vulkanResources.commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eHost, - vk::PipelineStageFlagBits::eComputeShader, - vk::DependencyFlags(), - hostToDeviceBarrier, - nullptr, - nullptr - ); - - // Step 1: Integrate forces and velocities - vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.integratePipeline); - vulkanResources.commandBuffer.dispatch((rigidBodies.size() + 63) / 64, 1, 1); - - // Memory barrier to ensure integration is complete before collision detection - vk::MemoryBarrier memoryBarrier; - memoryBarrier.srcAccessMask = vk::AccessFlagBits::eShaderWrite; - memoryBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - - vulkanResources.commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eComputeShader, - vk::PipelineStageFlagBits::eComputeShader, - vk::DependencyFlags(), - memoryBarrier, - nullptr, - nullptr - ); - - // Step 2: Broad-phase collision detection - vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.broadPhasePipeline); - uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; - // Dispatch number of workgroups matching [numthreads(64,1,1)] in BroadPhaseCS - // One workgroup has 64 threads, each processes one pair by index - uint32_t broadPhaseThreads = (numPairs + 63) / 64; - vulkanResources.commandBuffer.dispatch(std::max(1u, broadPhaseThreads), 1, 1); - - // Memory barrier to ensure the broad phase is complete before the narrow phase - vulkanResources.commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eComputeShader, - vk::PipelineStageFlagBits::eComputeShader, - vk::DependencyFlags(), - memoryBarrier, - nullptr, - nullptr - ); - - // Step 3: Narrow-phase collision detection - vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.narrowPhasePipeline); - // Dispatch enough threads to process all potential collision pairs found by broad-phase - // The shader will check counterBuffer[0] to determine the actual number of pairs to process - uint32_t narrowPhaseThreads = (maxGPUCollisions + 63) / 64; - vulkanResources.commandBuffer.dispatch(narrowPhaseThreads, 1, 1); - - // Memory barrier to ensure the narrow phase is complete before resolution - vulkanResources.commandBuffer.pipelineBarrier( - vk::PipelineStageFlagBits::eComputeShader, - vk::PipelineStageFlagBits::eComputeShader, - vk::DependencyFlags(), - memoryBarrier, - nullptr, - nullptr - ); - - // Step 4: Collision resolution - vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.resolvePipeline); - uint32_t resolveThreads = (maxGPUCollisions + 63) / 64; - vulkanResources.commandBuffer.dispatch(resolveThreads, 1, 1); - - // End command buffer - vulkanResources.commandBuffer.end(); - - // Reset fence before submitting new work - const vk::raii::Device& device = renderer->GetRaiiDevice(); - device.resetFences(*vulkanResources.computeFence); - - // Submit the command buffer with the dedicated fence for synchronization - vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; - renderer->SubmitToComputeQueue(cmdBuffer, *vulkanResources.computeFence); - - // Read back physics data from the GPU (fence wait moved to ReadbackGPUPhysicsData) - ReadbackGPUPhysicsData(); +void PhysicsSystem::SimulatePhysicsOnGPU(const std::chrono::milliseconds deltaTime) const +{ + if (!renderer) + { + fprintf(stderr, "SimulatePhysicsOnGPU: No renderer available"); + return; + } + + // Validate Vulkan resources before using them + if (*vulkanResources.broadPhasePipeline == VK_NULL_HANDLE || *vulkanResources.narrowPhasePipeline == VK_NULL_HANDLE || + *vulkanResources.integratePipeline == VK_NULL_HANDLE || *vulkanResources.pipelineLayout == VK_NULL_HANDLE || + vulkanResources.descriptorSets.empty() || *vulkanResources.physicsBuffer == VK_NULL_HANDLE || + *vulkanResources.counterBuffer == VK_NULL_HANDLE || *vulkanResources.paramsBuffer == VK_NULL_HANDLE) + { + return; + } + + // Update physics data on the GPU + UpdateGPUPhysicsData(deltaTime); + + // Reset the command buffer before beginning (required for reuse) + vulkanResources.commandBuffer.reset(); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo; + beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; + + vulkanResources.commandBuffer.begin(beginInfo); + + vulkanResources.commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + *vulkanResources.pipelineLayout, + 0, + **vulkanResources.descriptorSets.data(), + nullptr); + + // Add a memory barrier to ensure all host-written buffer data (uniform + storage) is visible to compute shaders + // We use ShaderRead | ShaderWrite since compute will read and write storage buffers + vk::MemoryBarrier hostToDeviceBarrier; + hostToDeviceBarrier.srcAccessMask = vk::AccessFlagBits::eHostWrite; + hostToDeviceBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; + + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eHost, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + hostToDeviceBarrier, + nullptr, + nullptr); + + // Step 1: Integrate forces and velocities + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.integratePipeline); + vulkanResources.commandBuffer.dispatch((rigidBodies.size() + 63) / 64, 1, 1); + + // Memory barrier to ensure integration is complete before collision detection + vk::MemoryBarrier memoryBarrier; + memoryBarrier.srcAccessMask = vk::AccessFlagBits::eShaderWrite; + memoryBarrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr); + + // Step 2: Broad-phase collision detection + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.broadPhasePipeline); + uint32_t numPairs = (rigidBodies.size() * (rigidBodies.size() - 1)) / 2; + // Dispatch number of workgroups matching [numthreads(64,1,1)] in BroadPhaseCS + // One workgroup has 64 threads, each processes one pair by index + uint32_t broadPhaseThreads = (numPairs + 63) / 64; + vulkanResources.commandBuffer.dispatch(std::max(1u, broadPhaseThreads), 1, 1); + + // Memory barrier to ensure the broad phase is complete before the narrow phase + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr); + + // Step 3: Narrow-phase collision detection + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.narrowPhasePipeline); + // Dispatch enough threads to process all potential collision pairs found by broad-phase + // The shader will check counterBuffer[0] to determine the actual number of pairs to process + uint32_t narrowPhaseThreads = (maxGPUCollisions + 63) / 64; + vulkanResources.commandBuffer.dispatch(narrowPhaseThreads, 1, 1); + + // Memory barrier to ensure the narrow phase is complete before resolution + vulkanResources.commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + vk::DependencyFlags(), + memoryBarrier, + nullptr, + nullptr); + + // Step 4: Collision resolution + vulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *vulkanResources.resolvePipeline); + uint32_t resolveThreads = (maxGPUCollisions + 63) / 64; + vulkanResources.commandBuffer.dispatch(resolveThreads, 1, 1); + + // End command buffer + vulkanResources.commandBuffer.end(); + + // Reset fence before submitting new work + const vk::raii::Device &device = renderer->GetRaiiDevice(); + device.resetFences(*vulkanResources.computeFence); + + // Submit the command buffer with the dedicated fence for synchronization + vk::CommandBuffer cmdBuffer = *vulkanResources.commandBuffer; + renderer->SubmitToComputeQueue(cmdBuffer, *vulkanResources.computeFence); + + // Read back physics data from the GPU (fence wait moved to ReadbackGPUPhysicsData) + ReadbackGPUPhysicsData(); } -void PhysicsSystem::CleanupMarkedBodies() { - // Remove rigid bodies that are marked for removal - auto it = rigidBodies.begin(); - while (it != rigidBodies.end()) { - auto concreteRigidBody = dynamic_cast(it->get()); - if (concreteRigidBody && concreteRigidBody->markedForRemoval) { - it = rigidBodies.erase(it); - } else { - ++it; - } - } +void PhysicsSystem::CleanupMarkedBodies() +{ + // Remove rigid bodies that are marked for removal + auto it = rigidBodies.begin(); + while (it != rigidBodies.end()) + { + auto concreteRigidBody = dynamic_cast(it->get()); + if (concreteRigidBody && concreteRigidBody->markedForRemoval) + { + it = rigidBodies.erase(it); + } + else + { + ++it; + } + } } diff --git a/attachments/simple_engine/physics_system.h b/attachments/simple_engine/physics_system.h index bb21429e..c70ee932 100644 --- a/attachments/simple_engine/physics_system.h +++ b/attachments/simple_engine/physics_system.h @@ -1,12 +1,28 @@ +/* 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 class Entity; class Renderer; @@ -14,173 +30,179 @@ class Renderer; /** * @brief Enum for different collision shapes. */ -enum class CollisionShape { - Box, - Sphere, - Capsule, - Mesh +enum class CollisionShape +{ + Box, + Sphere, + Capsule, + Mesh }; /** * @brief Class representing a rigid body for physics simulation. */ -class RigidBody { -public: - /** - * @brief Default constructor. - */ - RigidBody() = default; - - /** - * @brief Destructor for proper cleanup. - */ - virtual ~RigidBody() = default; - - /** - * @brief Set the position of the rigid body. - * @param position The position. - */ - virtual void SetPosition(const glm::vec3& position) = 0; - - /** - * @brief Set the rotation of the rigid body. - * @param rotation The rotation quaternion. - */ - virtual void SetRotation(const glm::quat& rotation) = 0; - - /** - * @brief Set the scale of the rigid body. - * @param scale The scale. - */ - virtual void SetScale(const glm::vec3& scale) = 0; - - /** - * @brief Set the mass of the rigid body. - * @param mass The mass. - */ - virtual void SetMass(float mass) = 0; - - /** - * @brief Set the restitution (bounciness) of the rigid body. - * @param restitution The restitution (0.0f to 1.0f). - */ - virtual void SetRestitution(float restitution) = 0; - - /** - * @brief Set the friction of the rigid body. - * @param friction The friction (0.0f to 1.0f). - */ - virtual void SetFriction(float friction) = 0; - - /** - * @brief Apply a force to the rigid body. - * @param force The force vector. - * @param localPosition The local position to apply the force at. - */ - virtual void ApplyForce(const glm::vec3& force, const glm::vec3& localPosition = glm::vec3(0.0f)) = 0; - - /** - * @brief Apply an impulse to the rigid body. - * @param impulse The impulse vector. - * @param localPosition The local position to apply the impulse at. - */ - virtual void ApplyImpulse(const glm::vec3& impulse, const glm::vec3& localPosition = glm::vec3(0.0f)) = 0; - - /** - * @brief Set the linear velocity of the rigid body. - * @param velocity The linear velocity. - */ - virtual void SetLinearVelocity(const glm::vec3& velocity) = 0; - - /** - * @brief Set the angular velocity of the rigid body. - * @param velocity The angular velocity. - */ - virtual void SetAngularVelocity(const glm::vec3& velocity) = 0; - - /** - * @brief Get the position of the rigid body. - * @return The position. - */ - [[nodiscard]] virtual glm::vec3 GetPosition() const = 0; - - /** - * @brief Get the rotation of the rigid body. - * @return The rotation quaternion. - */ - [[nodiscard]] virtual glm::quat GetRotation() const = 0; - - /** - * @brief Get the linear velocity of the rigid body. - * @return The linear velocity. - */ - [[nodiscard]] virtual glm::vec3 GetLinearVelocity() const = 0; - - /** - * @brief Get the angular velocity of the rigid body. - * @return The angular velocity. - */ - [[nodiscard]] virtual glm::vec3 GetAngularVelocity() const = 0; - - /** - * @brief Set whether the rigid body is kinematic. - * @param kinematic Whether the rigid body is kinematic. - */ - virtual void SetKinematic(bool kinematic) = 0; - - /** - * @brief Check if the rigid body is kinematic. - * @return True if kinematic, false otherwise. - */ - [[nodiscard]] virtual bool IsKinematic() const = 0; +class RigidBody +{ + public: + /** + * @brief Default constructor. + */ + RigidBody() = default; + + /** + * @brief Destructor for proper cleanup. + */ + virtual ~RigidBody() = default; + + /** + * @brief Set the position of the rigid body. + * @param position The position. + */ + virtual void SetPosition(const glm::vec3 &position) = 0; + + /** + * @brief Set the rotation of the rigid body. + * @param rotation The rotation quaternion. + */ + virtual void SetRotation(const glm::quat &rotation) = 0; + + /** + * @brief Set the scale of the rigid body. + * @param scale The scale. + */ + virtual void SetScale(const glm::vec3 &scale) = 0; + + /** + * @brief Set the mass of the rigid body. + * @param mass The mass. + */ + virtual void SetMass(float mass) = 0; + + /** + * @brief Set the restitution (bounciness) of the rigid body. + * @param restitution The restitution (0.0f to 1.0f). + */ + virtual void SetRestitution(float restitution) = 0; + + /** + * @brief Set the friction of the rigid body. + * @param friction The friction (0.0f to 1.0f). + */ + virtual void SetFriction(float friction) = 0; + + /** + * @brief Apply a force to the rigid body. + * @param force The force vector. + * @param localPosition The local position to apply the force at. + */ + virtual void ApplyForce(const glm::vec3 &force, const glm::vec3 &localPosition = glm::vec3(0.0f)) = 0; + + /** + * @brief Apply an impulse to the rigid body. + * @param impulse The impulse vector. + * @param localPosition The local position to apply the impulse at. + */ + virtual void ApplyImpulse(const glm::vec3 &impulse, const glm::vec3 &localPosition = glm::vec3(0.0f)) = 0; + + /** + * @brief Set the linear velocity of the rigid body. + * @param velocity The linear velocity. + */ + virtual void SetLinearVelocity(const glm::vec3 &velocity) = 0; + + /** + * @brief Set the angular velocity of the rigid body. + * @param velocity The angular velocity. + */ + virtual void SetAngularVelocity(const glm::vec3 &velocity) = 0; + + /** + * @brief Get the position of the rigid body. + * @return The position. + */ + [[nodiscard]] virtual glm::vec3 GetPosition() const = 0; + + /** + * @brief Get the rotation of the rigid body. + * @return The rotation quaternion. + */ + [[nodiscard]] virtual glm::quat GetRotation() const = 0; + + /** + * @brief Get the linear velocity of the rigid body. + * @return The linear velocity. + */ + [[nodiscard]] virtual glm::vec3 GetLinearVelocity() const = 0; + + /** + * @brief Get the angular velocity of the rigid body. + * @return The angular velocity. + */ + [[nodiscard]] virtual glm::vec3 GetAngularVelocity() const = 0; + + /** + * @brief Set whether the rigid body is kinematic. + * @param kinematic Whether the rigid body is kinematic. + */ + virtual void SetKinematic(bool kinematic) = 0; + + /** + * @brief Check if the rigid body is kinematic. + * @return True if kinematic, false otherwise. + */ + [[nodiscard]] virtual bool IsKinematic() const = 0; }; /** * @brief Structure for GPU physics data. */ -struct GPUPhysicsData { - glm::vec4 position; // xyz = position, w = inverse mass - glm::vec4 rotation; // quaternion - glm::vec4 linearVelocity; // xyz = velocity, w = restitution - glm::vec4 angularVelocity; // xyz = angular velocity, w = friction - glm::vec4 force; // xyz = force, w = is kinematic (0 or 1) - glm::vec4 torque; // xyz = torque, w = use gravity (0 or 1) - glm::vec4 colliderData; // type-specific data (e.g., radius for spheres) - glm::vec4 colliderData2; // additional collider data (e.g., box half extents) +struct GPUPhysicsData +{ + glm::vec4 position; // xyz = position, w = inverse mass + glm::vec4 rotation; // quaternion + glm::vec4 linearVelocity; // xyz = velocity, w = restitution + glm::vec4 angularVelocity; // xyz = angular velocity, w = friction + glm::vec4 force; // xyz = force, w = is kinematic (0 or 1) + glm::vec4 torque; // xyz = torque, w = use gravity (0 or 1) + glm::vec4 colliderData; // type-specific data (e.g., radius for spheres) + glm::vec4 colliderData2; // additional collider data (e.g., box half extents) }; /** * @brief Structure for GPU collision data. */ -struct GPUCollisionData { - uint32_t bodyA; - uint32_t bodyB; - glm::vec4 contactNormal; // xyz = normal, w = penetration depth - glm::vec4 contactPoint; // xyz = contact point, w = unused +struct GPUCollisionData +{ + uint32_t bodyA; + uint32_t bodyB; + glm::vec4 contactNormal; // xyz = normal, w = penetration depth + glm::vec4 contactPoint; // xyz = contact point, w = unused }; /** * @brief Structure for physics simulation parameters. */ -struct PhysicsParams { - float deltaTime; // Time step - 4 bytes - uint32_t numBodies; // Number of rigid bodies - 4 bytes - uint32_t maxCollisions; // Maximum number of collisions - 4 bytes - float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes - glm::vec4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes - // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) +struct PhysicsParams +{ + float deltaTime; // Time step - 4 bytes + uint32_t numBodies; // Number of rigid bodies - 4 bytes + uint32_t maxCollisions; // Maximum number of collisions - 4 bytes + float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes + glm::vec4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes + // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) }; /** * @brief Structure to store collision prediction data for a ray-based collision system. */ -struct CollisionPrediction { - float collisionTime = -1.0f; // Time within deltaTime when the collision occurs (-1 = no collision) - glm::vec3 collisionPoint; // World position where collision occurs - glm::vec3 collisionNormal; // Surface normal at collision point - glm::vec3 newVelocity; // Predicted velocity after bounce - Entity* hitEntity = nullptr; // Entity that was hit - bool isValid = false; // Whether this prediction is valid +struct CollisionPrediction +{ + float collisionTime = -1.0f; // Time within deltaTime when the collision occurs (-1 = no collision) + glm::vec3 collisionPoint; // World position where collision occurs + glm::vec3 collisionNormal; // Surface normal at collision point + glm::vec3 newVelocity; // Predicted velocity after bounce + Entity *hitEntity = nullptr; // Entity that was hit + bool isValid = false; // Whether this prediction is valid }; /** @@ -190,216 +212,234 @@ struct CollisionPrediction { * @see en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc * @see en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc */ -class PhysicsSystem { -public: - /** - * @brief Default constructor. - */ - PhysicsSystem() = default; - - // Constructor-based initialization replacing separate Initialize/Set* calls - explicit PhysicsSystem(Renderer* _renderer, bool enableGPU = true) { - SetRenderer(_renderer); - SetGPUAccelerationEnabled(enableGPU); - if (!Initialize()) { - throw std::runtime_error("PhysicsSystem: initialization failed"); - } - } - - /** - * @brief Destructor for proper cleanup. - */ - ~PhysicsSystem(); - - /** - * @brief Initialize the physics system. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(); - - /** - * @brief Update the physics system. - * @param deltaTime The time elapsed since the last update. - */ - void Update(std::chrono::milliseconds deltaTime); - - /** - * @brief Create a rigid body. - * @param entity The entity to attach the rigid body to. - * @param shape The collision shape. - * @param mass The mass. - * @return Pointer to the created rigid body, or nullptr if creation failed. - */ - RigidBody* CreateRigidBody(Entity* entity, CollisionShape shape, float mass); - - /** - * @brief Remove a rigid body. - * @param rigidBody The rigid body to remove. - * @return True if removal was successful, false otherwise. - */ - bool RemoveRigidBody(RigidBody* rigidBody); - - /** - * @brief Set the gravity of the physics world. - * @param _gravity The gravity vector. - */ - void SetGravity(const glm::vec3& _gravity); - - /** - * @brief Get the gravity of the physics world. - * @return The gravity vector. - */ - [[nodiscard]] glm::vec3 GetGravity() const; - - /** - * @brief Perform a raycast. - * @param origin The origin of the ray. - * @param direction The direction of the ray. - * @param maxDistance The maximum distance of the ray. - * @param hitPosition Output parameter for the hit position. - * @param hitNormal Output parameter for the hit normal. - * @param hitEntity Output parameter for the hit entity. - * @return True if the ray hit something, false otherwise. - */ - bool Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, - glm::vec3* hitPosition, glm::vec3* hitNormal, Entity** hitEntity) const; - - /** - * @brief Enable or disable GPU acceleration. - * @param enabled Whether GPU acceleration is enabled. - */ - void SetGPUAccelerationEnabled(bool enabled) { - // Enforce GPU-only policy: disabling GPU acceleration is not allowed in this project. - // Ignore attempts to disable and keep GPU acceleration enabled. - gpuAccelerationEnabled = true; - } - - /** - * @brief Check if GPU acceleration is enabled. - * @return True, if GPU acceleration is enabled, false otherwise. - */ - [[nodiscard]] bool IsGPUAccelerationEnabled() const { return gpuAccelerationEnabled; } - - /** - * @brief Set the maximum number of objects that can be simulated on the GPU. - * @param maxObjects The maximum number of objects. - */ - void SetMaxGPUObjects(uint32_t maxObjects) { maxGPUObjects = maxObjects; } - - /** - * @brief Set the renderer to use during GPU acceleration. - * @param _renderer The renderer. - */ - void SetRenderer(Renderer* _renderer) { renderer = _renderer; } - - /** - * @brief Set the current camera position for geometry-relative ball checking. - * @param _cameraPosition The current camera position. - */ - void SetCameraPosition(const glm::vec3& _cameraPosition) { cameraPosition = _cameraPosition; } - - // Thread-safe enqueue for rigid body creation from any thread - void EnqueueRigidBodyCreation(Entity* entity, - CollisionShape shape, - float mass, - bool kinematic, - float restitution, - float friction); - -private: - /** - * @brief Clean up rigid bodies that are marked for removal. - */ - void CleanupMarkedBodies(); - - // Pending rigid body creations queued from background threads - struct PendingCreation { - Entity* entity; - CollisionShape shape; - float mass; - bool kinematic; - float restitution; - float friction; - }; - std::mutex pendingMutex; - std::vector pendingCreations; - - // Rigid bodies - mutable std::mutex rigidBodiesMutex; // Protect concurrent access to rigidBodies - std::vector> rigidBodies; - - // Gravity - glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f); - - // Whether the physics system is initialized - bool initialized = false; - - // GPU acceleration - bool gpuAccelerationEnabled = false; - uint32_t maxGPUObjects = 1024; - uint32_t maxGPUCollisions = 4096; - Renderer* renderer = nullptr; - - // Camera position for geometry-relative ball checking - glm::vec3 cameraPosition = glm::vec3(0.0f, 0.0f, 0.0f); - - // Vulkan resources for physics simulation - struct VulkanResources { - // Shader modules - vk::raii::ShaderModule integrateShaderModule = nullptr; - vk::raii::ShaderModule broadPhaseShaderModule = nullptr; - vk::raii::ShaderModule narrowPhaseShaderModule = nullptr; - vk::raii::ShaderModule resolveShaderModule = nullptr; - - // Pipeline layouts and compute pipelines - vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; - vk::raii::PipelineLayout pipelineLayout = nullptr; - vk::raii::Pipeline integratePipeline = nullptr; - vk::raii::Pipeline broadPhasePipeline = nullptr; - vk::raii::Pipeline narrowPhasePipeline = nullptr; - vk::raii::Pipeline resolvePipeline = nullptr; - - // Descriptor pool and sets - vk::raii::DescriptorPool descriptorPool = nullptr; - std::vector descriptorSets; - - // Buffers for physics data - vk::raii::Buffer physicsBuffer = nullptr; - vk::raii::DeviceMemory physicsBufferMemory = nullptr; - vk::raii::Buffer collisionBuffer = nullptr; - vk::raii::DeviceMemory collisionBufferMemory = nullptr; - vk::raii::Buffer pairBuffer = nullptr; - vk::raii::DeviceMemory pairBufferMemory = nullptr; - vk::raii::Buffer counterBuffer = nullptr; - vk::raii::DeviceMemory counterBufferMemory = nullptr; - vk::raii::Buffer paramsBuffer = nullptr; - vk::raii::DeviceMemory paramsBufferMemory = nullptr; - - // Persistent mapped memory pointers for improved performance - void* persistentPhysicsMemory = nullptr; - void* persistentCounterMemory = nullptr; - void* persistentParamsMemory = nullptr; - - // Command buffer for compute operations - vk::raii::CommandPool commandPool = nullptr; - vk::raii::CommandBuffer commandBuffer = nullptr; - - // Dedicated fence for compute synchronization - vk::raii::Fence computeFence = nullptr; - }; - - VulkanResources vulkanResources; - - // Initialize Vulkan resources for physics simulation - bool InitializeVulkanResources(); - void CleanupVulkanResources(); - - // Update physics data on the GPU - void UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const; - - // Read back physics data from the GPU - void ReadbackGPUPhysicsData() const; - - // Perform GPU-accelerated physics simulation - void SimulatePhysicsOnGPU(std::chrono::milliseconds deltaTime) const; +class PhysicsSystem +{ + public: + /** + * @brief Default constructor. + */ + PhysicsSystem() = default; + + // Constructor-based initialization replacing separate Initialize/Set* calls + explicit PhysicsSystem(Renderer *_renderer, bool enableGPU = true) + { + SetRenderer(_renderer); + SetGPUAccelerationEnabled(enableGPU); + if (!Initialize()) + { + throw std::runtime_error("PhysicsSystem: initialization failed"); + } + } + + /** + * @brief Destructor for proper cleanup. + */ + ~PhysicsSystem(); + + /** + * @brief Initialize the physics system. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(); + + /** + * @brief Update the physics system. + * @param deltaTime The time elapsed since the last update. + */ + void Update(std::chrono::milliseconds deltaTime); + + /** + * @brief Create a rigid body. + * @param entity The entity to attach the rigid body to. + * @param shape The collision shape. + * @param mass The mass. + * @return Pointer to the created rigid body, or nullptr if creation failed. + */ + RigidBody *CreateRigidBody(Entity *entity, CollisionShape shape, float mass); + + /** + * @brief Remove a rigid body. + * @param rigidBody The rigid body to remove. + * @return True if removal was successful, false otherwise. + */ + bool RemoveRigidBody(RigidBody *rigidBody); + + /** + * @brief Set the gravity of the physics world. + * @param _gravity The gravity vector. + */ + void SetGravity(const glm::vec3 &_gravity); + + /** + * @brief Get the gravity of the physics world. + * @return The gravity vector. + */ + [[nodiscard]] glm::vec3 GetGravity() const; + + /** + * @brief Perform a raycast. + * @param origin The origin of the ray. + * @param direction The direction of the ray. + * @param maxDistance The maximum distance of the ray. + * @param hitPosition Output parameter for the hit position. + * @param hitNormal Output parameter for the hit normal. + * @param hitEntity Output parameter for the hit entity. + * @return True if the ray hit something, false otherwise. + */ + bool Raycast(const glm::vec3 &origin, const glm::vec3 &direction, float maxDistance, + glm::vec3 *hitPosition, glm::vec3 *hitNormal, Entity **hitEntity) const; + + /** + * @brief Enable or disable GPU acceleration. + * @param enabled Whether GPU acceleration is enabled. + */ + void SetGPUAccelerationEnabled(bool enabled) + { + // Enforce GPU-only policy: disabling GPU acceleration is not allowed in this project. + // Ignore attempts to disable and keep GPU acceleration enabled. + gpuAccelerationEnabled = true; + } + + /** + * @brief Check if GPU acceleration is enabled. + * @return True, if GPU acceleration is enabled, false otherwise. + */ + [[nodiscard]] bool IsGPUAccelerationEnabled() const + { + return gpuAccelerationEnabled; + } + + /** + * @brief Set the maximum number of objects that can be simulated on the GPU. + * @param maxObjects The maximum number of objects. + */ + void SetMaxGPUObjects(uint32_t maxObjects) + { + maxGPUObjects = maxObjects; + } + + /** + * @brief Set the renderer to use during GPU acceleration. + * @param _renderer The renderer. + */ + void SetRenderer(Renderer *_renderer) + { + renderer = _renderer; + } + + /** + * @brief Set the current camera position for geometry-relative ball checking. + * @param _cameraPosition The current camera position. + */ + void SetCameraPosition(const glm::vec3 &_cameraPosition) + { + cameraPosition = _cameraPosition; + } + + // Thread-safe enqueue for rigid body creation from any thread + void EnqueueRigidBodyCreation(Entity *entity, + CollisionShape shape, + float mass, + bool kinematic, + float restitution, + float friction); + + private: + /** + * @brief Clean up rigid bodies that are marked for removal. + */ + void CleanupMarkedBodies(); + + // Pending rigid body creations queued from background threads + struct PendingCreation + { + Entity *entity; + CollisionShape shape; + float mass; + bool kinematic; + float restitution; + float friction; + }; + std::mutex pendingMutex; + std::vector pendingCreations; + + // Rigid bodies + mutable std::mutex rigidBodiesMutex; // Protect concurrent access to rigidBodies + std::vector> rigidBodies; + + // Gravity + glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f); + + // Whether the physics system is initialized + bool initialized = false; + + // GPU acceleration + bool gpuAccelerationEnabled = false; + uint32_t maxGPUObjects = 1024; + uint32_t maxGPUCollisions = 4096; + Renderer *renderer = nullptr; + + // Camera position for geometry-relative ball checking + glm::vec3 cameraPosition = glm::vec3(0.0f, 0.0f, 0.0f); + + // Vulkan resources for physics simulation + struct VulkanResources + { + // Shader modules + vk::raii::ShaderModule integrateShaderModule = nullptr; + vk::raii::ShaderModule broadPhaseShaderModule = nullptr; + vk::raii::ShaderModule narrowPhaseShaderModule = nullptr; + vk::raii::ShaderModule resolveShaderModule = nullptr; + + // Pipeline layouts and compute pipelines + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline integratePipeline = nullptr; + vk::raii::Pipeline broadPhasePipeline = nullptr; + vk::raii::Pipeline narrowPhasePipeline = nullptr; + vk::raii::Pipeline resolvePipeline = nullptr; + + // Descriptor pool and sets + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + + // Buffers for physics data + vk::raii::Buffer physicsBuffer = nullptr; + vk::raii::DeviceMemory physicsBufferMemory = nullptr; + vk::raii::Buffer collisionBuffer = nullptr; + vk::raii::DeviceMemory collisionBufferMemory = nullptr; + vk::raii::Buffer pairBuffer = nullptr; + vk::raii::DeviceMemory pairBufferMemory = nullptr; + vk::raii::Buffer counterBuffer = nullptr; + vk::raii::DeviceMemory counterBufferMemory = nullptr; + vk::raii::Buffer paramsBuffer = nullptr; + vk::raii::DeviceMemory paramsBufferMemory = nullptr; + + // Persistent mapped memory pointers for improved performance + void *persistentPhysicsMemory = nullptr; + void *persistentCounterMemory = nullptr; + void *persistentParamsMemory = nullptr; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + + // Dedicated fence for compute synchronization + vk::raii::Fence computeFence = nullptr; + }; + + VulkanResources vulkanResources; + + // Initialize Vulkan resources for physics simulation + bool InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Update physics data on the GPU + void UpdateGPUPhysicsData(std::chrono::milliseconds deltaTime) const; + + // Read back physics data from the GPU + void ReadbackGPUPhysicsData() const; + + // Perform GPU-accelerated physics simulation + void SimulatePhysicsOnGPU(std::chrono::milliseconds deltaTime) const; }; diff --git a/attachments/simple_engine/pipeline.cpp b/attachments/simple_engine/pipeline.cpp index ac44c3b3..2e09ffcf 100644 --- a/attachments/simple_engine/pipeline.cpp +++ b/attachments/simple_engine/pipeline.cpp @@ -1,741 +1,720 @@ +/* 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 "pipeline.h" #include "mesh_component.h" -#include #include +#include // Constructor -Pipeline::Pipeline(VulkanDevice& device, SwapChain& swapChain) - : device(device), swapChain(swapChain) { +Pipeline::Pipeline(VulkanDevice &device, SwapChain &swapChain) : + device(device), swapChain(swapChain) +{ } // Destructor -Pipeline::~Pipeline() { - // RAII will handle destruction +Pipeline::~Pipeline() +{ + // RAII will handle destruction } // Create descriptor set layout -bool Pipeline::createDescriptorSetLayout() { - try { - // Create descriptor set layout bindings - std::array bindings = { - vk::DescriptorSetLayoutBinding{ - .binding = 0, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, - .pImmutableSamplers = nullptr - }, - vk::DescriptorSetLayoutBinding{ - .binding = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eFragment, - .pImmutableSamplers = nullptr - } - }; - - // Create descriptor set layout - vk::DescriptorSetLayoutCreateInfo layoutInfo{ - .bindingCount = static_cast(bindings.size()), - .pBindings = bindings.data() - }; - - descriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), layoutInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; - return false; - } +bool Pipeline::createDescriptorSetLayout() +{ + try + { + // Create descriptor set layout bindings + std::array bindings = { + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr}, + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr}}; + + // Create descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data()}; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), 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 Pipeline::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 (StructuredBuffer) - vk::DescriptorSetLayoutBinding{ - .binding = 6, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eFragment, - .pImmutableSamplers = nullptr - } - }; - - // Create descriptor set layout - vk::DescriptorSetLayoutCreateInfo layoutInfo{ - .bindingCount = static_cast(bindings.size()), - .pBindings = bindings.data() - }; - - pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), layoutInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; - return false; - } +bool Pipeline::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 (StructuredBuffer) + vk::DescriptorSetLayoutBinding{ + .binding = 6, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .pImmutableSamplers = nullptr}}; + + // Create descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data()}; + + pbrDescriptorSetLayout = vk::raii::DescriptorSetLayout(device.getDevice(), layoutInfo); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create PBR descriptor set layout: " << e.what() << std::endl; + return false; + } } // Create graphics pipeline -bool Pipeline::createGraphicsPipeline() { - try { - // Read shader code - auto vertShaderCode = readFile("shaders/texturedMesh.spv"); - auto fragShaderCode = readFile("shaders/texturedMesh.spv"); - - // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); - - // Create shader stage info - vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, - .pName = "VSMain" - }; - - vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, - .pName = "PSMain" - }; - - vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - - // Create vertex input info - vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 0, - .pVertexBindingDescriptions = nullptr, - .vertexAttributeDescriptionCount = 0, - .pVertexAttributeDescriptions = nullptr - }; - - // Create input assembly info - vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ - .topology = vk::PrimitiveTopology::eTriangleList, - .primitiveRestartEnable = VK_FALSE - }; - - // Create viewport state info - vk::Viewport viewport{ - .x = 0.0f, - .y = 0.0f, - .width = static_cast(swapChain.getSwapChainExtent().width), - .height = static_cast(swapChain.getSwapChainExtent().height), - .minDepth = 0.0f, - .maxDepth = 1.0f - }; - - vk::Rect2D scissor{ - .offset = {0, 0}, - .extent = swapChain.getSwapChainExtent() - }; - - vk::PipelineViewportStateCreateInfo viewportState{ - .viewportCount = 1, - .pViewports = &viewport, - .scissorCount = 1, - .pScissors = &scissor - }; - - // Create rasterization state info - vk::PipelineRasterizationStateCreateInfo rasterizer{ - .depthClampEnable = VK_FALSE, - .rasterizerDiscardEnable = VK_FALSE, - .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, - .frontFace = vk::FrontFace::eCounterClockwise, - .depthBiasEnable = VK_FALSE, - .depthBiasConstantFactor = 0.0f, - .depthBiasClamp = 0.0f, - .depthBiasSlopeFactor = 0.0f, - .lineWidth = 1.0f - }; - - // Create multisample state info - vk::PipelineMultisampleStateCreateInfo multisampling{ - .rasterizationSamples = vk::SampleCountFlagBits::e1, - .sampleShadingEnable = VK_FALSE, - .minSampleShading = 1.0f, - .pSampleMask = nullptr, - .alphaToCoverageEnable = VK_FALSE, - .alphaToOneEnable = 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, - .front = {}, - .back = {}, - .minDepthBounds = 0.0f, - .maxDepthBounds = 1.0f - }; - - // Create color blend attachment state - vk::PipelineColorBlendAttachmentState colorBlendAttachment{ - .blendEnable = VK_FALSE, - .srcColorBlendFactor = vk::BlendFactor::eOne, - .dstColorBlendFactor = vk::BlendFactor::eZero, - .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 - std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; - vk::PipelineColorBlendStateCreateInfo colorBlending{ - .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eCopy, - .attachmentCount = 1, - .pAttachments = &colorBlendAttachment, - .blendConstants = blendConstants - }; - - // 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.getDevice(), pipelineLayoutInfo); - - // Create graphics pipeline - vk::GraphicsPipelineCreateInfo pipelineInfo{ - .stageCount = 2, - .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, - .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, - .pDepthStencilState = &depthStencil, - .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, - .layout = *pipelineLayout, - .renderPass = nullptr, - .subpass = 0, - .basePipelineHandle = nullptr, - .basePipelineIndex = -1 - }; - - // Create pipeline with dynamic rendering - vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); - vk::PipelineRenderingCreateInfo renderingInfo{ - .colorAttachmentCount = 1, - .pColorAttachmentFormats = &swapChainFormat, - .depthAttachmentFormat = vk::Format::eD32Sfloat, - .stencilAttachmentFormat = vk::Format::eUndefined - }; - - pipelineInfo.pNext = &renderingInfo; - - graphicsPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create graphics pipeline: " << e.what() << std::endl; - return false; - } +bool Pipeline::createGraphicsPipeline() +{ + try + { + // Read shader code + auto vertShaderCode = readFile("shaders/texturedMesh.spv"); + auto fragShaderCode = readFile("shaders/texturedMesh.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain"}; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain"}; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr}; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE}; + + // Create viewport state info + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent()}; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor}; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f}; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = 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, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f}; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .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 + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants}; + + // 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.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1}; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined}; + + pipelineInfo.pNext = &renderingInfo; + + graphicsPipeline = vk::raii::Pipeline(device.getDevice(), 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 Pipeline::createPBRPipeline() { - try { - // Create PBR descriptor set layout - if (!createPBRDescriptorSetLayout()) { - return false; - } - - // Read shader code - auto vertShaderCode = readFile("shaders/pbr.spv"); - auto fragShaderCode = readFile("shaders/pbr.spv"); - - // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); - - // Create shader stage info - vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, - .pName = "VSMain" - }; - - vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, - .pName = "PSMain" // Changed from FSMain to PSMain to match the shader - }; - - vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - - // Define vertex and instance binding descriptions using MeshComponent layouts - auto vertexBinding = Vertex::getBindingDescription(); - auto instanceBinding = InstanceData::getBindingDescription(); - std::array bindingDescriptions = { vertexBinding, instanceBinding }; - - // Define vertex and instance attribute descriptions - auto vertexAttrArray = Vertex::getAttributeDescriptions(); - auto instanceAttrArray = InstanceData::getAttributeDescriptions(); - std::array attributeDescriptions{}; - // Copy vertex attributes (0..3) - for (size_t i = 0; i < vertexAttrArray.size(); ++i) { - attributeDescriptions[i] = vertexAttrArray[i]; - } - // Copy instance attributes (4..10) - for (size_t i = 0; i < instanceAttrArray.size(); ++i) { - attributeDescriptions[vertexAttrArray.size() + i] = instanceAttrArray[i]; - } - - // Create vertex input info - vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), - .pVertexBindingDescriptions = bindingDescriptions.data(), - .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::Viewport viewport{ - .x = 0.0f, - .y = 0.0f, - .width = static_cast(swapChain.getSwapChainExtent().width), - .height = static_cast(swapChain.getSwapChainExtent().height), - .minDepth = 0.0f, - .maxDepth = 1.0f - }; - - vk::Rect2D scissor{ - .offset = {0, 0}, - .extent = swapChain.getSwapChainExtent() - }; - - vk::PipelineViewportStateCreateInfo viewportState{ - .viewportCount = 1, - .pViewports = &viewport, - .scissorCount = 1, - .pScissors = &scissor - }; - - // Create rasterization state info - vk::PipelineRasterizationStateCreateInfo rasterizer{ - .depthClampEnable = VK_FALSE, - .rasterizerDiscardEnable = VK_FALSE, - .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, - .frontFace = vk::FrontFace::eCounterClockwise, - .depthBiasEnable = VK_FALSE, - .depthBiasConstantFactor = 0.0f, - .depthBiasClamp = 0.0f, - .depthBiasSlopeFactor = 0.0f, - .lineWidth = 1.0f - }; - - // Create multisample state info - vk::PipelineMultisampleStateCreateInfo multisampling{ - .rasterizationSamples = vk::SampleCountFlagBits::e1, - .sampleShadingEnable = VK_FALSE, - .minSampleShading = 1.0f, - .pSampleMask = nullptr, - .alphaToCoverageEnable = VK_TRUE, - .alphaToOneEnable = 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, - .front = {}, - .back = {}, - .minDepthBounds = 0.0f, - .maxDepthBounds = 1.0f - }; - - // Create color blend attachment state - vk::PipelineColorBlendAttachmentState colorBlendAttachment{ - .blendEnable = VK_FALSE, - .srcColorBlendFactor = vk::BlendFactor::eOne, - .dstColorBlendFactor = vk::BlendFactor::eZero, - .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 - std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; - vk::PipelineColorBlendStateCreateInfo colorBlending{ - .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eCopy, - .attachmentCount = 1, - .pAttachments = &colorBlendAttachment, - .blendConstants = blendConstants - }; - - // 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 using the PBR descriptor set layout - vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ - .setLayoutCount = 1, - .pSetLayouts = &*pbrDescriptorSetLayout, // Use PBR descriptor set layout - .pushConstantRangeCount = 1, - .pPushConstantRanges = &pushConstantRange - }; - - pbrPipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); - - // Create graphics pipeline - vk::GraphicsPipelineCreateInfo pipelineInfo{ - .stageCount = 2, - .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, - .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, - .pDepthStencilState = &depthStencil, - .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, - .layout = *pbrPipelineLayout, - .renderPass = nullptr, - .subpass = 0, - .basePipelineHandle = nullptr, - .basePipelineIndex = -1 - }; - - // Create pipeline with dynamic rendering - vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); - vk::PipelineRenderingCreateInfo renderingInfo{ - .colorAttachmentCount = 1, - .pColorAttachmentFormats = &swapChainFormat, - .depthAttachmentFormat = vk::Format::eD32Sfloat, - .stencilAttachmentFormat = vk::Format::eUndefined - }; - - pipelineInfo.pNext = &renderingInfo; - - pbrGraphicsPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; - return false; - } +bool Pipeline::createPBRPipeline() +{ + try + { + // Create PBR descriptor set layout + if (!createPBRDescriptorSetLayout()) + { + return false; + } + + // Read shader code + auto vertShaderCode = readFile("shaders/pbr.spv"); + auto fragShaderCode = readFile("shaders/pbr.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain"}; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain" // Changed from FSMain to PSMain to match the shader + }; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Define vertex and instance binding descriptions using MeshComponent layouts + auto vertexBinding = Vertex::getBindingDescription(); + auto instanceBinding = InstanceData::getBindingDescription(); + std::array bindingDescriptions = {vertexBinding, instanceBinding}; + + // Define vertex and instance attribute descriptions + auto vertexAttrArray = Vertex::getAttributeDescriptions(); + auto instanceAttrArray = InstanceData::getAttributeDescriptions(); + std::array attributeDescriptions{}; + // Copy vertex attributes (0..3) + for (size_t i = 0; i < vertexAttrArray.size(); ++i) + { + attributeDescriptions[i] = vertexAttrArray[i]; + } + // Copy instance attributes (4..10) + for (size_t i = 0; i < instanceAttrArray.size(); ++i) + { + attributeDescriptions[vertexAttrArray.size() + i] = instanceAttrArray[i]; + } + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()), + .pVertexBindingDescriptions = bindingDescriptions.data(), + .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::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent()}; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor}; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f}; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_TRUE, + .alphaToOneEnable = 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, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f}; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .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 + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants}; + + // 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 using the PBR descriptor set layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*pbrDescriptorSetLayout, // Use PBR descriptor set layout + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange}; + + pbrPipelineLayout = vk::raii::PipelineLayout(device.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1}; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined}; + + pipelineInfo.pNext = &renderingInfo; + + pbrGraphicsPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create PBR pipeline: " << e.what() << std::endl; + return false; + } } // Create lighting pipeline -bool Pipeline::createLightingPipeline() { - try { - // Read shader code - auto vertShaderCode = readFile("shaders/lighting.spv"); - auto fragShaderCode = readFile("shaders/lighting.spv"); - - // Create shader modules - vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); - vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); - - // Create shader stage info - vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eVertex, - .module = *vertShaderModule, - .pName = "VSMain" - }; - - vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eFragment, - .module = *fragShaderModule, - .pName = "PSMain" - }; - - vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - - // Create vertex input info - vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ - .vertexBindingDescriptionCount = 0, - .pVertexBindingDescriptions = nullptr, - .vertexAttributeDescriptionCount = 0, - .pVertexAttributeDescriptions = nullptr - }; - - // Create input assembly info - vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ - .topology = vk::PrimitiveTopology::eTriangleList, - .primitiveRestartEnable = VK_FALSE - }; - - // Create viewport state info - vk::Viewport viewport{ - .x = 0.0f, - .y = 0.0f, - .width = static_cast(swapChain.getSwapChainExtent().width), - .height = static_cast(swapChain.getSwapChainExtent().height), - .minDepth = 0.0f, - .maxDepth = 1.0f - }; - - vk::Rect2D scissor{ - .offset = {0, 0}, - .extent = swapChain.getSwapChainExtent() - }; - - vk::PipelineViewportStateCreateInfo viewportState{ - .viewportCount = 1, - .pViewports = &viewport, - .scissorCount = 1, - .pScissors = &scissor - }; - - // Create rasterization state info - vk::PipelineRasterizationStateCreateInfo rasterizer{ - .depthClampEnable = VK_FALSE, - .rasterizerDiscardEnable = VK_FALSE, - .polygonMode = vk::PolygonMode::eFill, - .cullMode = vk::CullModeFlagBits::eBack, - .frontFace = vk::FrontFace::eCounterClockwise, - .depthBiasEnable = VK_FALSE, - .depthBiasConstantFactor = 0.0f, - .depthBiasClamp = 0.0f, - .depthBiasSlopeFactor = 0.0f, - .lineWidth = 1.0f - }; - - // Create multisample state info - vk::PipelineMultisampleStateCreateInfo multisampling{ - .rasterizationSamples = vk::SampleCountFlagBits::e1, - .sampleShadingEnable = VK_FALSE, - .minSampleShading = 1.0f, - .pSampleMask = nullptr, - .alphaToCoverageEnable = VK_FALSE, - .alphaToOneEnable = 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, - .front = {}, - .back = {}, - .minDepthBounds = 0.0f, - .maxDepthBounds = 1.0f - }; - - // Create color blend attachment state - vk::PipelineColorBlendAttachmentState colorBlendAttachment{ - .blendEnable = VK_FALSE, - .srcColorBlendFactor = vk::BlendFactor::eOne, - .dstColorBlendFactor = vk::BlendFactor::eZero, - .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 - std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; - vk::PipelineColorBlendStateCreateInfo colorBlending{ - .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eCopy, - .attachmentCount = 1, - .pAttachments = &colorBlendAttachment, - .blendConstants = blendConstants - }; - - // 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.getDevice(), pipelineLayoutInfo); - - // Create graphics pipeline - vk::GraphicsPipelineCreateInfo pipelineInfo{ - .stageCount = 2, - .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, - .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, - .pDepthStencilState = &depthStencil, - .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, - .layout = *lightingPipelineLayout, - .renderPass = nullptr, - .subpass = 0, - .basePipelineHandle = nullptr, - .basePipelineIndex = -1 - }; - - // Create pipeline with dynamic rendering - vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); - vk::PipelineRenderingCreateInfo renderingInfo{ - .colorAttachmentCount = 1, - .pColorAttachmentFormats = &swapChainFormat, - .depthAttachmentFormat = vk::Format::eD32Sfloat, - .stencilAttachmentFormat = vk::Format::eUndefined - }; - - pipelineInfo.pNext = &renderingInfo; - - lightingPipeline = vk::raii::Pipeline(device.getDevice(), nullptr, pipelineInfo); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create lighting pipeline: " << e.what() << std::endl; - return false; - } +bool Pipeline::createLightingPipeline() +{ + try + { + // Read shader code + auto vertShaderCode = readFile("shaders/lighting.spv"); + auto fragShaderCode = readFile("shaders/lighting.spv"); + + // Create shader modules + vk::raii::ShaderModule vertShaderModule = createShaderModule(vertShaderCode); + vk::raii::ShaderModule fragShaderModule = createShaderModule(fragShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eVertex, + .module = *vertShaderModule, + .pName = "VSMain"}; + + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eFragment, + .module = *fragShaderModule, + .pName = "PSMain"}; + + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + // Create vertex input info + vk::PipelineVertexInputStateCreateInfo vertexInputInfo{ + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr}; + + // Create input assembly info + vk::PipelineInputAssemblyStateCreateInfo inputAssembly{ + .topology = vk::PrimitiveTopology::eTriangleList, + .primitiveRestartEnable = VK_FALSE}; + + // Create viewport state info + vk::Viewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(swapChain.getSwapChainExtent().width), + .height = static_cast(swapChain.getSwapChainExtent().height), + .minDepth = 0.0f, + .maxDepth = 1.0f}; + + vk::Rect2D scissor{ + .offset = {0, 0}, + .extent = swapChain.getSwapChainExtent()}; + + vk::PipelineViewportStateCreateInfo viewportState{ + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor}; + + // Create rasterization state info + vk::PipelineRasterizationStateCreateInfo rasterizer{ + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f}; + + // Create multisample state info + vk::PipelineMultisampleStateCreateInfo multisampling{ + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 1.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = 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, + .front = {}, + .back = {}, + .minDepthBounds = 0.0f, + .maxDepthBounds = 1.0f}; + + // Create color blend attachment state + vk::PipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = vk::BlendFactor::eOne, + .dstColorBlendFactor = vk::BlendFactor::eZero, + .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 + std::array blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}; + vk::PipelineColorBlendStateCreateInfo colorBlending{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + .blendConstants = blendConstants}; + + // 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.getDevice(), pipelineLayoutInfo); + + // Create graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo{ + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = *lightingPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1}; + + // Create pipeline with dynamic rendering + vk::Format swapChainFormat = swapChain.getSwapChainImageFormat(); + vk::PipelineRenderingCreateInfo renderingInfo{ + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainFormat, + .depthAttachmentFormat = vk::Format::eD32Sfloat, + .stencilAttachmentFormat = vk::Format::eUndefined}; + + pipelineInfo.pNext = &renderingInfo; + + lightingPipeline = vk::raii::Pipeline(device.getDevice(), 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 -void Pipeline::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material) { - commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, material); +void Pipeline::pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties &material) +{ + commandBuffer.pushConstants(*pbrPipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, material); } // Create shader module -vk::raii::ShaderModule Pipeline::createShaderModule(const std::vector& code) { - vk::ShaderModuleCreateInfo createInfo{ - .codeSize = code.size(), - .pCode = reinterpret_cast(code.data()) - }; +vk::raii::ShaderModule Pipeline::createShaderModule(const std::vector &code) +{ + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data())}; - return vk::raii::ShaderModule(device.getDevice(), createInfo); + return vk::raii::ShaderModule(device.getDevice(), createInfo); } // Read file -std::vector Pipeline::readFile(const std::string& filename) { - std::ifstream file(filename, std::ios::ate | std::ios::binary); +std::vector Pipeline::readFile(const std::string &filename) +{ + std::ifstream file(filename, std::ios::ate | std::ios::binary); - if (!file.is_open()) { - throw std::runtime_error("Failed to open file: " + filename); - } + if (!file.is_open()) + { + throw std::runtime_error("Failed to open file: " + filename); + } - size_t fileSize = file.tellg(); - std::vector buffer(fileSize); + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); - file.seekg(0); - file.read(buffer.data(), fileSize); - file.close(); + file.seekg(0); + file.read(buffer.data(), fileSize); + file.close(); - return buffer; + return buffer; } diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h index a6da24e4..86353a88 100644 --- a/attachments/simple_engine/pipeline.h +++ b/attachments/simple_engine/pipeline.h @@ -1,184 +1,235 @@ +/* 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 #define GLM_FORCE_RADIANS #include #include -#include "vulkan_device.h" #include "swap_chain.h" +#include "vulkan_device.h" /** * @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; // Index of refraction for transmission +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; // Index of refraction for transmission }; /** * @brief Class for managing Vulkan pipelines. */ -class Pipeline { -public: - /** - * @brief Constructor. - * @param device The Vulkan device. - * @param swapChain The swap chain. - */ - Pipeline(VulkanDevice& device, SwapChain& swapChain); - - /** - * @brief Destructor. - */ - ~Pipeline(); - - /** - * @brief Create the descriptor set layout. - * @return True if the descriptor set layout was created successfully, false otherwise. - */ - bool createDescriptorSetLayout(); - - /** - * @brief Create the PBR descriptor set layout. - * @return True if the PBR descriptor set layout was created successfully, false otherwise. - */ - bool createPBRDescriptorSetLayout(); - - /** - * @brief Create the graphics pipeline. - * @return True if the graphics pipeline was created successfully, false otherwise. - */ - bool createGraphicsPipeline(); - - /** - * @brief Create the PBR pipeline. - * @return True if the PBR pipeline was created successfully, false otherwise. - */ - bool createPBRPipeline(); - - /** - * @brief Create the lighting pipeline. - * @return True if the lighting pipeline was created successfully, false otherwise. - */ - bool createLightingPipeline(); - - /** - * @brief Push material properties to a command buffer. - * @param commandBuffer The command buffer. - * @param material The material properties. - */ - void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties& material); - - /** - * @brief Get the descriptor set layout. - * @return The descriptor set layout. - */ - vk::raii::DescriptorSetLayout& getDescriptorSetLayout() { return descriptorSetLayout; } - - /** - * @brief Get the pipeline layout. - * @return The pipeline layout. - */ - vk::raii::PipelineLayout& getPipelineLayout() { return pipelineLayout; } - - /** - * @brief Get the graphics pipeline. - * @return The graphics pipeline. - */ - vk::raii::Pipeline& getGraphicsPipeline() { return graphicsPipeline; } - - /** - * @brief Get the PBR pipeline layout. - * @return The PBR pipeline layout. - */ - vk::raii::PipelineLayout& getPBRPipelineLayout() { return pbrPipelineLayout; } - - /** - * @brief Get the PBR graphics pipeline. - * @return The PBR graphics pipeline. - */ - vk::raii::Pipeline& getPBRGraphicsPipeline() { return pbrGraphicsPipeline; } - - /** - * @brief Get the lighting pipeline layout. - * @return The lighting pipeline layout. - */ - vk::raii::PipelineLayout& getLightingPipelineLayout() { return lightingPipelineLayout; } - - /** - * @brief Get the lighting pipeline. - * @return The lighting pipeline. - */ - vk::raii::Pipeline& getLightingPipeline() { return lightingPipeline; } - - /** - * @brief Get the compute pipeline layout. - * @return The compute pipeline layout. - */ - vk::raii::PipelineLayout& getComputePipelineLayout() { return computePipelineLayout; } - - /** - * @brief Get the compute pipeline. - * @return The compute pipeline. - */ - vk::raii::Pipeline& getComputePipeline() { return computePipeline; } - - /** - * @brief Get the compute descriptor set layout. - * @return The compute descriptor set layout. - */ - vk::raii::DescriptorSetLayout& getComputeDescriptorSetLayout() { return computeDescriptorSetLayout; } - - /** - * @brief Get the PBR descriptor set layout. - * @return The PBR descriptor set layout. - */ - vk::raii::DescriptorSetLayout& getPBRDescriptorSetLayout() { return pbrDescriptorSetLayout; } - -private: - // Vulkan device - VulkanDevice& device; - - // Swap chain - SwapChain& swapChain; - - // Pipelines - vk::raii::PipelineLayout pipelineLayout = nullptr; - vk::raii::Pipeline graphicsPipeline = nullptr; - vk::raii::PipelineLayout pbrPipelineLayout = nullptr; - vk::raii::Pipeline pbrGraphicsPipeline = nullptr; - vk::raii::PipelineLayout lightingPipelineLayout = nullptr; - vk::raii::Pipeline lightingPipeline = nullptr; - - // Compute pipeline - vk::raii::PipelineLayout computePipelineLayout = nullptr; - vk::raii::Pipeline computePipeline = nullptr; - vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; - - // Descriptor set layouts - vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; - vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; - - // Helper functions - vk::raii::ShaderModule createShaderModule(const std::vector& code); - std::vector readFile(const std::string& filename); +class Pipeline +{ + public: + /** + * @brief Constructor. + * @param device The Vulkan device. + * @param swapChain The swap chain. + */ + Pipeline(VulkanDevice &device, SwapChain &swapChain); + + /** + * @brief Destructor. + */ + ~Pipeline(); + + /** + * @brief Create the descriptor set layout. + * @return True if the descriptor set layout was created successfully, false otherwise. + */ + bool createDescriptorSetLayout(); + + /** + * @brief Create the PBR descriptor set layout. + * @return True if the PBR descriptor set layout was created successfully, false otherwise. + */ + bool createPBRDescriptorSetLayout(); + + /** + * @brief Create the graphics pipeline. + * @return True if the graphics pipeline was created successfully, false otherwise. + */ + bool createGraphicsPipeline(); + + /** + * @brief Create the PBR pipeline. + * @return True if the PBR pipeline was created successfully, false otherwise. + */ + bool createPBRPipeline(); + + /** + * @brief Create the lighting pipeline. + * @return True if the lighting pipeline was created successfully, false otherwise. + */ + bool createLightingPipeline(); + + /** + * @brief Push material properties to a command buffer. + * @param commandBuffer The command buffer. + * @param material The material properties. + */ + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const MaterialProperties &material); + + /** + * @brief Get the descriptor set layout. + * @return The descriptor set layout. + */ + vk::raii::DescriptorSetLayout &getDescriptorSetLayout() + { + return descriptorSetLayout; + } + + /** + * @brief Get the pipeline layout. + * @return The pipeline layout. + */ + vk::raii::PipelineLayout &getPipelineLayout() + { + return pipelineLayout; + } + + /** + * @brief Get the graphics pipeline. + * @return The graphics pipeline. + */ + vk::raii::Pipeline &getGraphicsPipeline() + { + return graphicsPipeline; + } + + /** + * @brief Get the PBR pipeline layout. + * @return The PBR pipeline layout. + */ + vk::raii::PipelineLayout &getPBRPipelineLayout() + { + return pbrPipelineLayout; + } + + /** + * @brief Get the PBR graphics pipeline. + * @return The PBR graphics pipeline. + */ + vk::raii::Pipeline &getPBRGraphicsPipeline() + { + return pbrGraphicsPipeline; + } + + /** + * @brief Get the lighting pipeline layout. + * @return The lighting pipeline layout. + */ + vk::raii::PipelineLayout &getLightingPipelineLayout() + { + return lightingPipelineLayout; + } + + /** + * @brief Get the lighting pipeline. + * @return The lighting pipeline. + */ + vk::raii::Pipeline &getLightingPipeline() + { + return lightingPipeline; + } + + /** + * @brief Get the compute pipeline layout. + * @return The compute pipeline layout. + */ + vk::raii::PipelineLayout &getComputePipelineLayout() + { + return computePipelineLayout; + } + + /** + * @brief Get the compute pipeline. + * @return The compute pipeline. + */ + vk::raii::Pipeline &getComputePipeline() + { + return computePipeline; + } + + /** + * @brief Get the compute descriptor set layout. + * @return The compute descriptor set layout. + */ + vk::raii::DescriptorSetLayout &getComputeDescriptorSetLayout() + { + return computeDescriptorSetLayout; + } + + /** + * @brief Get the PBR descriptor set layout. + * @return The PBR descriptor set layout. + */ + vk::raii::DescriptorSetLayout &getPBRDescriptorSetLayout() + { + return pbrDescriptorSetLayout; + } + + private: + // Vulkan device + VulkanDevice &device; + + // Swap chain + SwapChain &swapChain; + + // Pipelines + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + vk::raii::PipelineLayout pbrPipelineLayout = nullptr; + vk::raii::Pipeline pbrGraphicsPipeline = nullptr; + vk::raii::PipelineLayout lightingPipelineLayout = nullptr; + vk::raii::Pipeline lightingPipeline = nullptr; + + // Compute pipeline + vk::raii::PipelineLayout computePipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorSetLayout computeDescriptorSetLayout = nullptr; + + // Descriptor set layouts + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; + + // Helper functions + vk::raii::ShaderModule createShaderModule(const std::vector &code); + std::vector readFile(const std::string &filename); }; diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp index 29302ecd..fcb37a70 100644 --- a/attachments/simple_engine/platform.cpp +++ b/attachments/simple_engine/platform.cpp @@ -1,3 +1,19 @@ +/* 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 "platform.h" #include @@ -5,508 +21,580 @@ #if defined(PLATFORM_ANDROID) // Android platform implementation -AndroidPlatform::AndroidPlatform(android_app* androidApp) - : app(androidApp) { - // Set up the app's user data - app->userData = this; - - // Set up the command callback - app->onAppCmd = [](android_app* app, int32_t cmd) { - auto* platform = static_cast(app->userData); - - switch (cmd) { - case APP_CMD_INIT_WINDOW: - if (app->window != nullptr) { - // Get the window dimensions - ANativeWindow* window = app->window; - platform->width = ANativeWindow_getWidth(window); - platform->height = ANativeWindow_getHeight(window); - platform->windowResized = true; - - // Call the resize callback if set - if (platform->resizeCallback) { - platform->resizeCallback(platform->width, platform->height); - } - } - break; - - case APP_CMD_TERM_WINDOW: - // Window is being hidden or closed - break; - - case APP_CMD_WINDOW_RESIZED: - if (app->window != nullptr) { - // Get the new window dimensions - ANativeWindow* window = app->window; - platform->width = ANativeWindow_getWidth(window); - platform->height = ANativeWindow_getHeight(window); - platform->windowResized = true; - - // Call the resize callback if set - if (platform->resizeCallback) { - platform->resizeCallback(platform->width, platform->height); - } - } - break; - - default: - break; - } - }; +AndroidPlatform::AndroidPlatform(android_app *androidApp) : + app(androidApp) +{ + // Set up the app's user data + app->userData = this; + + // Set up the command callback + app->onAppCmd = [](android_app *app, int32_t cmd) { + auto *platform = static_cast(app->userData); + + switch (cmd) + { + case APP_CMD_INIT_WINDOW: + if (app->window != nullptr) + { + // Get the window dimensions + ANativeWindow *window = app->window; + platform->width = ANativeWindow_getWidth(window); + platform->height = ANativeWindow_getHeight(window); + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) + { + platform->resizeCallback(platform->width, platform->height); + } + } + break; + + case APP_CMD_TERM_WINDOW: + // Window is being hidden or closed + break; + + case APP_CMD_WINDOW_RESIZED: + if (app->window != nullptr) + { + // Get the new window dimensions + ANativeWindow *window = app->window; + platform->width = ANativeWindow_getWidth(window); + platform->height = ANativeWindow_getHeight(window); + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) + { + platform->resizeCallback(platform->width, platform->height); + } + } + break; + + default: + break; + } + }; } -bool AndroidPlatform::Initialize(const std::string& appName, int requestedWidth, int requestedHeight) { - // On Android, the window dimensions are determined by the device - if (app->window != nullptr) { - width = ANativeWindow_getWidth(app->window); - height = ANativeWindow_getHeight(app->window); +bool AndroidPlatform::Initialize(const std::string &appName, int requestedWidth, int requestedHeight) +{ + // On Android, the window dimensions are determined by the device + if (app->window != nullptr) + { + width = ANativeWindow_getWidth(app->window); + height = ANativeWindow_getHeight(app->window); - // Get device information for performance optimizations - // This is important for mobile development to adapt to different device capabilities - DetectDeviceCapabilities(); + // Get device information for performance optimizations + // This is important for mobile development to adapt to different device capabilities + DetectDeviceCapabilities(); - // Set up power-saving mode based on battery level - SetupPowerSavingMode(); + // Set up power-saving mode based on battery level + SetupPowerSavingMode(); - // Initialize touch input handling - InitializeTouchInput(); + // Initialize touch input handling + InitializeTouchInput(); - return true; - } - return false; + return true; + } + return false; } -void AndroidPlatform::Cleanup() { - // Nothing to clean up for Android +void AndroidPlatform::Cleanup() +{ + // Nothing to clean up for Android } -bool AndroidPlatform::ProcessEvents() { - // Process Android events - int events; - android_poll_source* source; - - // Poll for events with a timeout of 0 (non-blocking) - while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0) { - if (source != nullptr) { - source->process(app, source); - } - - // Check if we are exiting - if (app->destroyRequested != 0) { - return false; - } - } - - return true; +bool AndroidPlatform::ProcessEvents() +{ + // Process Android events + int events; + android_poll_source *source; + + // Poll for events with a timeout of 0 (non-blocking) + while (ALooper_pollAll(0, nullptr, &events, (void **) &source) >= 0) + { + if (source != nullptr) + { + source->process(app, source); + } + + // Check if we are exiting + if (app->destroyRequested != 0) + { + return false; + } + } + + return true; } -bool AndroidPlatform::HasWindowResized() { - bool resized = windowResized; - windowResized = false; - return resized; +bool AndroidPlatform::HasWindowResized() +{ + bool resized = windowResized; + windowResized = false; + return resized; } -bool AndroidPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) { - if (app->window == nullptr) { - return false; - } +bool AndroidPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) +{ + if (app->window == nullptr) + { + return false; + } - VkAndroidSurfaceCreateInfoKHR createInfo{}; - createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; - createInfo.window = app->window; + VkAndroidSurfaceCreateInfoKHR createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + createInfo.window = app->window; - if (vkCreateAndroidSurfaceKHR(instance, &createInfo, nullptr, surface) != VK_SUCCESS) { - return false; - } + if (vkCreateAndroidSurfaceKHR(instance, &createInfo, nullptr, surface) != VK_SUCCESS) + { + return false; + } - return true; + return true; } -void AndroidPlatform::SetResizeCallback(std::function callback) { - resizeCallback = std::move(callback); +void AndroidPlatform::SetResizeCallback(std::function callback) +{ + resizeCallback = std::move(callback); } -void AndroidPlatform::SetMouseCallback(std::function callback) { - mouseCallback = std::move(callback); +void AndroidPlatform::SetMouseCallback(std::function callback) +{ + mouseCallback = std::move(callback); } -void AndroidPlatform::SetKeyboardCallback(std::function callback) { - keyboardCallback = std::move(callback); +void AndroidPlatform::SetKeyboardCallback(std::function callback) +{ + keyboardCallback = std::move(callback); } -void AndroidPlatform::SetCharCallback(std::function callback) { - charCallback = std::move(callback); +void AndroidPlatform::SetCharCallback(std::function callback) +{ + charCallback = std::move(callback); } -void AndroidPlatform::SetWindowTitle(const std::string& title) { - // No-op on Android - mobile apps don't have window titles - (void)title; // Suppress unused parameter warning +void AndroidPlatform::SetWindowTitle(const std::string &title) +{ + // No-op on Android - mobile apps don't have window titles + (void) title; // Suppress unused parameter warning } -void AndroidPlatform::DetectDeviceCapabilities() { - if (!app) { - return; - } - - // Get API level - JNIEnv* env = nullptr; - app->activity->vm->AttachCurrentThread(&env, nullptr); - if (env) { - // Get Build.VERSION.SDK_INT - jclass versionClass = env->FindClass("android/os/Build$VERSION"); - jfieldID sdkFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I"); - deviceCapabilities.apiLevel = env->GetStaticIntField(versionClass, sdkFieldID); - - // Get device model and manufacturer - jclass buildClass = env->FindClass("android/os/Build"); - jfieldID modelFieldID = env->GetStaticFieldID(buildClass, "MODEL", "Ljava/lang/String;"); - jfieldID manufacturerFieldID = env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;"); - - jstring modelJString = (jstring)env->GetStaticObjectField(buildClass, modelFieldID); - jstring manufacturerJString = (jstring)env->GetStaticObjectField(buildClass, manufacturerFieldID); - - const char* modelChars = env->GetStringUTFChars(modelJString, nullptr); - const char* manufacturerChars = env->GetStringUTFChars(manufacturerJString, nullptr); - - deviceCapabilities.deviceModel = modelChars; - deviceCapabilities.deviceManufacturer = manufacturerChars; - - env->ReleaseStringUTFChars(modelJString, modelChars); - env->ReleaseStringUTFChars(manufacturerJString, manufacturerChars); - - // Get CPU cores - jclass runtimeClass = env->FindClass("java/lang/Runtime"); - jmethodID getRuntime = env->GetStaticMethodID(runtimeClass, "getRuntime", "()Ljava/lang/Runtime;"); - jobject runtime = env->CallStaticObjectMethod(runtimeClass, getRuntime); - jmethodID availableProcessors = env->GetMethodID(runtimeClass, "availableProcessors", "()I"); - deviceCapabilities.cpuCores = env->CallIntMethod(runtime, availableProcessors); - - // Get total memory - jclass activityManagerClass = env->FindClass("android/app/ActivityManager"); - jclass memoryInfoClass = env->FindClass("android/app/ActivityManager$MemoryInfo"); - jmethodID memoryInfoConstructor = env->GetMethodID(memoryInfoClass, "", "()V"); - jobject memoryInfo = env->NewObject(memoryInfoClass, memoryInfoConstructor); - - jmethodID getSystemService = env->GetMethodID(env->GetObjectClass(app->activity->clazz), - "getSystemService", - "(Ljava/lang/String;)Ljava/lang/Object;"); - jstring serviceStr = env->NewStringUTF("activity"); - jobject activityManager = env->CallObjectMethod(app->activity->clazz, getSystemService, serviceStr); - - jmethodID getMemoryInfo = env->GetMethodID(activityManagerClass, "getMemoryInfo", - "(Landroid/app/ActivityManager$MemoryInfo;)V"); - env->CallVoidMethod(activityManager, getMemoryInfo, memoryInfo); - - jfieldID totalMemField = env->GetFieldID(memoryInfoClass, "totalMem", "J"); - deviceCapabilities.totalMemory = env->GetLongField(memoryInfo, totalMemField); - - env->DeleteLocalRef(serviceStr); - - // Check Vulkan support - // In a real implementation, this would check for Vulkan support and available extensions - deviceCapabilities.supportsVulkan = true; - deviceCapabilities.supportsVulkan11 = deviceCapabilities.apiLevel >= 28; // Android 9 (Pie) - deviceCapabilities.supportsVulkan12 = deviceCapabilities.apiLevel >= 29; // Android 10 - - // Add some common Vulkan extensions for mobile - deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); - deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_MAINTENANCE1_EXTENSION_NAME); - deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); - - if (deviceCapabilities.apiLevel >= 28) { - deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME); - deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME); - } - - app->activity->vm->DetachCurrentThread(); - } - - LOGI("Device capabilities detected:"); - LOGI(" API Level: %d", deviceCapabilities.apiLevel); - LOGI(" Device: %s by %s", deviceCapabilities.deviceModel.c_str(), deviceCapabilities.deviceManufacturer.c_str()); - LOGI(" CPU Cores: %d", deviceCapabilities.cpuCores); - LOGI(" Total Memory: %lld bytes", (long long)deviceCapabilities.totalMemory); - LOGI(" Vulkan Support: %s", deviceCapabilities.supportsVulkan ? "Yes" : "No"); - LOGI(" Vulkan 1.1 Support: %s", deviceCapabilities.supportsVulkan11 ? "Yes" : "No"); - LOGI(" Vulkan 1.2 Support: %s", deviceCapabilities.supportsVulkan12 ? "Yes" : "No"); +void AndroidPlatform::DetectDeviceCapabilities() +{ + if (!app) + { + return; + } + + // Get API level + JNIEnv *env = nullptr; + app->activity->vm->AttachCurrentThread(&env, nullptr); + if (env) + { + // Get Build.VERSION.SDK_INT + jclass versionClass = env->FindClass("android/os/Build$VERSION"); + jfieldID sdkFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I"); + deviceCapabilities.apiLevel = env->GetStaticIntField(versionClass, sdkFieldID); + + // Get device model and manufacturer + jclass buildClass = env->FindClass("android/os/Build"); + jfieldID modelFieldID = env->GetStaticFieldID(buildClass, "MODEL", "Ljava/lang/String;"); + jfieldID manufacturerFieldID = env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;"); + + jstring modelJString = (jstring) env->GetStaticObjectField(buildClass, modelFieldID); + jstring manufacturerJString = (jstring) env->GetStaticObjectField(buildClass, manufacturerFieldID); + + const char *modelChars = env->GetStringUTFChars(modelJString, nullptr); + const char *manufacturerChars = env->GetStringUTFChars(manufacturerJString, nullptr); + + deviceCapabilities.deviceModel = modelChars; + deviceCapabilities.deviceManufacturer = manufacturerChars; + + env->ReleaseStringUTFChars(modelJString, modelChars); + env->ReleaseStringUTFChars(manufacturerJString, manufacturerChars); + + // Get CPU cores + jclass runtimeClass = env->FindClass("java/lang/Runtime"); + jmethodID getRuntime = env->GetStaticMethodID(runtimeClass, "getRuntime", "()Ljava/lang/Runtime;"); + jobject runtime = env->CallStaticObjectMethod(runtimeClass, getRuntime); + jmethodID availableProcessors = env->GetMethodID(runtimeClass, "availableProcessors", "()I"); + deviceCapabilities.cpuCores = env->CallIntMethod(runtime, availableProcessors); + + // Get total memory + jclass activityManagerClass = env->FindClass("android/app/ActivityManager"); + jclass memoryInfoClass = env->FindClass("android/app/ActivityManager$MemoryInfo"); + jmethodID memoryInfoConstructor = env->GetMethodID(memoryInfoClass, "", "()V"); + jobject memoryInfo = env->NewObject(memoryInfoClass, memoryInfoConstructor); + + jmethodID getSystemService = env->GetMethodID(env->GetObjectClass(app->activity->clazz), + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;"); + jstring serviceStr = env->NewStringUTF("activity"); + jobject activityManager = env->CallObjectMethod(app->activity->clazz, getSystemService, serviceStr); + + jmethodID getMemoryInfo = env->GetMethodID(activityManagerClass, "getMemoryInfo", + "(Landroid/app/ActivityManager$MemoryInfo;)V"); + env->CallVoidMethod(activityManager, getMemoryInfo, memoryInfo); + + jfieldID totalMemField = env->GetFieldID(memoryInfoClass, "totalMem", "J"); + deviceCapabilities.totalMemory = env->GetLongField(memoryInfo, totalMemField); + + env->DeleteLocalRef(serviceStr); + + // Check Vulkan support + // In a real implementation, this would check for Vulkan support and available extensions + deviceCapabilities.supportsVulkan = true; + deviceCapabilities.supportsVulkan11 = deviceCapabilities.apiLevel >= 28; // Android 9 (Pie) + deviceCapabilities.supportsVulkan12 = deviceCapabilities.apiLevel >= 29; // Android 10 + + // Add some common Vulkan extensions for mobile + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_MAINTENANCE1_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); + + if (deviceCapabilities.apiLevel >= 28) + { + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME); + deviceCapabilities.supportedVulkanExtensions.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME); + } + + app->activity->vm->DetachCurrentThread(); + } + + LOGI("Device capabilities detected:"); + LOGI(" API Level: %d", deviceCapabilities.apiLevel); + LOGI(" Device: %s by %s", deviceCapabilities.deviceModel.c_str(), deviceCapabilities.deviceManufacturer.c_str()); + LOGI(" CPU Cores: %d", deviceCapabilities.cpuCores); + LOGI(" Total Memory: %lld bytes", (long long) deviceCapabilities.totalMemory); + LOGI(" Vulkan Support: %s", deviceCapabilities.supportsVulkan ? "Yes" : "No"); + LOGI(" Vulkan 1.1 Support: %s", deviceCapabilities.supportsVulkan11 ? "Yes" : "No"); + LOGI(" Vulkan 1.2 Support: %s", deviceCapabilities.supportsVulkan12 ? "Yes" : "No"); } -void AndroidPlatform::SetupPowerSavingMode() { - if (!app) { - return; - } - - // Check battery level and status - JNIEnv* env = nullptr; - app->activity->vm->AttachCurrentThread(&env, nullptr); - if (env) { - // Get battery level - jclass intentFilterClass = env->FindClass("android/content/IntentFilter"); - jmethodID intentFilterConstructor = env->GetMethodID(intentFilterClass, "", "(Ljava/lang/String;)V"); - jstring actionBatteryChanged = env->NewStringUTF("android.intent.action.BATTERY_CHANGED"); - jobject filter = env->NewObject(intentFilterClass, intentFilterConstructor, actionBatteryChanged); - - jmethodID registerReceiver = env->GetMethodID(env->GetObjectClass(app->activity->clazz), - "registerReceiver", - "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;"); - jobject intent = env->CallObjectMethod(app->activity->clazz, registerReceiver, nullptr, filter); - - if (intent) { - // Get battery level - jclass intentClass = env->GetObjectClass(intent); - jmethodID getIntExtra = env->GetMethodID(intentClass, "getIntExtra", "(Ljava/lang/String;I)I"); - - jstring levelKey = env->NewStringUTF("level"); - jstring scaleKey = env->NewStringUTF("scale"); - jstring statusKey = env->NewStringUTF("status"); - - int level = env->CallIntMethod(intent, getIntExtra, levelKey, -1); - int scale = env->CallIntMethod(intent, getIntExtra, scaleKey, -1); - int status = env->CallIntMethod(intent, getIntExtra, statusKey, -1); - - env->DeleteLocalRef(levelKey); - env->DeleteLocalRef(scaleKey); - env->DeleteLocalRef(statusKey); - - if (level != -1 && scale != -1) { - float batteryPct = (float)level / (float)scale; - - // Enable power-saving mode if battery is low (below 20%) and not charging - // Status values: 2 = charging, 3 = discharging, 4 = not charging, 5 = full - bool isCharging = (status == 2 || status == 5); - - if (batteryPct < 0.2f && !isCharging) { - EnablePowerSavingMode(true); - LOGI("Battery level low (%.0f%%), enabling power-saving mode", batteryPct * 100.0f); - } else { - LOGI("Battery level: %.0f%%, %s", batteryPct * 100.0f, isCharging ? "charging" : "not charging"); - } - } - } - - env->DeleteLocalRef(actionBatteryChanged); - app->activity->vm->DetachCurrentThread(); - } +void AndroidPlatform::SetupPowerSavingMode() +{ + if (!app) + { + return; + } + + // Check battery level and status + JNIEnv *env = nullptr; + app->activity->vm->AttachCurrentThread(&env, nullptr); + if (env) + { + // Get battery level + jclass intentFilterClass = env->FindClass("android/content/IntentFilter"); + jmethodID intentFilterConstructor = env->GetMethodID(intentFilterClass, "", "(Ljava/lang/String;)V"); + jstring actionBatteryChanged = env->NewStringUTF("android.intent.action.BATTERY_CHANGED"); + jobject filter = env->NewObject(intentFilterClass, intentFilterConstructor, actionBatteryChanged); + + jmethodID registerReceiver = env->GetMethodID(env->GetObjectClass(app->activity->clazz), + "registerReceiver", + "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;"); + jobject intent = env->CallObjectMethod(app->activity->clazz, registerReceiver, nullptr, filter); + + if (intent) + { + // Get battery level + jclass intentClass = env->GetObjectClass(intent); + jmethodID getIntExtra = env->GetMethodID(intentClass, "getIntExtra", "(Ljava/lang/String;I)I"); + + jstring levelKey = env->NewStringUTF("level"); + jstring scaleKey = env->NewStringUTF("scale"); + jstring statusKey = env->NewStringUTF("status"); + + int level = env->CallIntMethod(intent, getIntExtra, levelKey, -1); + int scale = env->CallIntMethod(intent, getIntExtra, scaleKey, -1); + int status = env->CallIntMethod(intent, getIntExtra, statusKey, -1); + + env->DeleteLocalRef(levelKey); + env->DeleteLocalRef(scaleKey); + env->DeleteLocalRef(statusKey); + + if (level != -1 && scale != -1) + { + float batteryPct = (float) level / (float) scale; + + // Enable power-saving mode if battery is low (below 20%) and not charging + // Status values: 2 = charging, 3 = discharging, 4 = not charging, 5 = full + bool isCharging = (status == 2 || status == 5); + + if (batteryPct < 0.2f && !isCharging) + { + EnablePowerSavingMode(true); + LOGI("Battery level low (%.0f%%), enabling power-saving mode", batteryPct * 100.0f); + } + else + { + LOGI("Battery level: %.0f%%, %s", batteryPct * 100.0f, isCharging ? "charging" : "not charging"); + } + } + } + + env->DeleteLocalRef(actionBatteryChanged); + app->activity->vm->DetachCurrentThread(); + } } -void AndroidPlatform::InitializeTouchInput() { - if (!app) { - return; - } - - // Set up input handling for touch events - app->onInputEvent = [](android_app* app, AInputEvent* event) -> int32_t { - auto* platform = static_cast(app->userData); - - if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { - int32_t action = AMotionEvent_getAction(event); - uint32_t flags = action & AMOTION_EVENT_ACTION_MASK; - - // Handle multi-touch if enabled - int32_t pointerCount = AMotionEvent_getPointerCount(event); - if (platform->IsMultiTouchEnabled() && pointerCount > 1) { - // In a real implementation, this would handle multi-touch gestures - // For now, just log the number of touch points - LOGI("Multi-touch event with %d pointers", pointerCount); - } - - // Convert touch event to mouse event for the engine - if (platform->mouseCallback) { - float x = AMotionEvent_getX(event, 0); - float y = AMotionEvent_getY(event, 0); - - uint32_t buttons = 0; - if (flags == AMOTION_EVENT_ACTION_DOWN || flags == AMOTION_EVENT_ACTION_MOVE) { - buttons |= 0x01; // Left button - } - - platform->mouseCallback(x, y, buttons); - } - - return 1; // Event handled - } - - return 0; // Event not handled - }; - - LOGI("Touch input initialized"); +void AndroidPlatform::InitializeTouchInput() +{ + if (!app) + { + return; + } + + // Set up input handling for touch events + app->onInputEvent = [](android_app *app, AInputEvent *event) -> int32_t { + auto *platform = static_cast(app->userData); + + if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) + { + int32_t action = AMotionEvent_getAction(event); + uint32_t flags = action & AMOTION_EVENT_ACTION_MASK; + + // Handle multi-touch if enabled + int32_t pointerCount = AMotionEvent_getPointerCount(event); + if (platform->IsMultiTouchEnabled() && pointerCount > 1) + { + // In a real implementation, this would handle multi-touch gestures + // For now, just log the number of touch points + LOGI("Multi-touch event with %d pointers", pointerCount); + } + + // Convert touch event to mouse event for the engine + if (platform->mouseCallback) + { + float x = AMotionEvent_getX(event, 0); + float y = AMotionEvent_getY(event, 0); + + uint32_t buttons = 0; + if (flags == AMOTION_EVENT_ACTION_DOWN || flags == AMOTION_EVENT_ACTION_MOVE) + { + buttons |= 0x01; // Left button + } + + platform->mouseCallback(x, y, buttons); + } + + return 1; // Event handled + } + + return 0; // Event not handled + }; + + LOGI("Touch input initialized"); } -void AndroidPlatform::EnablePowerSavingMode(bool enable) { - powerSavingMode = enable; +void AndroidPlatform::EnablePowerSavingMode(bool enable) +{ + powerSavingMode = enable; - // In a real implementation, this would adjust rendering quality, update frequency, etc. - LOGI("Power-saving mode %s", enable ? "enabled" : "disabled"); + // In a real implementation, this would adjust rendering quality, update frequency, etc. + LOGI("Power-saving mode %s", enable ? "enabled" : "disabled"); - // Example of what would be done in a real implementation: - // - Reduce rendering resolution - // - Lower frame rate - // - Disable post-processing effects - // - Reduce draw distance - // - Use simpler shaders + // Example of what would be done in a real implementation: + // - Reduce rendering resolution + // - Lower frame rate + // - Disable post-processing effects + // - Reduce draw distance + // - Use simpler shaders } #else // Desktop platform implementation -bool DesktopPlatform::Initialize(const std::string& appName, int requestedWidth, int requestedHeight) { - // Initialize GLFW - if (!glfwInit()) { - throw std::runtime_error("Failed to initialize GLFW"); - } - - // GLFW was designed for OpenGL, so we need to tell it not to create an OpenGL context - glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - - // Create the window - window = glfwCreateWindow(requestedWidth, requestedHeight, appName.c_str(), nullptr, nullptr); - if (!window) { - glfwTerminate(); - throw std::runtime_error("Failed to create GLFW window"); - } - - // Set up the user pointer for callbacks - glfwSetWindowUserPointer(window, this); - - // Set up the callbacks - glfwSetFramebufferSizeCallback(window, WindowResizeCallback); - glfwSetCursorPosCallback(window, MousePositionCallback); - glfwSetMouseButtonCallback(window, MouseButtonCallback); - glfwSetKeyCallback(window, KeyCallback); - glfwSetCharCallback(window, CharCallback); - - // Get the initial window size - glfwGetFramebufferSize(window, &width, &height); - - return true; +bool DesktopPlatform::Initialize(const std::string &appName, int requestedWidth, int requestedHeight) +{ + // Initialize GLFW + if (!glfwInit()) + { + throw std::runtime_error("Failed to initialize GLFW"); + } + + // GLFW was designed for OpenGL, so we need to tell it not to create an OpenGL context + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + // Create the window + window = glfwCreateWindow(requestedWidth, requestedHeight, appName.c_str(), nullptr, nullptr); + if (!window) + { + glfwTerminate(); + throw std::runtime_error("Failed to create GLFW window"); + } + + // Set up the user pointer for callbacks + glfwSetWindowUserPointer(window, this); + + // Set up the callbacks + glfwSetFramebufferSizeCallback(window, WindowResizeCallback); + glfwSetCursorPosCallback(window, MousePositionCallback); + glfwSetMouseButtonCallback(window, MouseButtonCallback); + glfwSetKeyCallback(window, KeyCallback); + glfwSetCharCallback(window, CharCallback); + + // Get the initial window size + glfwGetFramebufferSize(window, &width, &height); + + return true; } -void DesktopPlatform::Cleanup() { - if (window) { - glfwDestroyWindow(window); - window = nullptr; - } +void DesktopPlatform::Cleanup() +{ + if (window) + { + glfwDestroyWindow(window); + window = nullptr; + } - glfwTerminate(); + glfwTerminate(); } -bool DesktopPlatform::ProcessEvents() { - // Process GLFW events - glfwPollEvents(); +bool DesktopPlatform::ProcessEvents() +{ + // Process GLFW events + glfwPollEvents(); - // Check if the window should close - return !glfwWindowShouldClose(window); + // Check if the window should close + return !glfwWindowShouldClose(window); } -bool DesktopPlatform::HasWindowResized() { - bool resized = windowResized; - windowResized = false; - return resized; +bool DesktopPlatform::HasWindowResized() +{ + bool resized = windowResized; + windowResized = false; + return resized; } -bool DesktopPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) { - if (glfwCreateWindowSurface(instance, window, nullptr, surface) != VK_SUCCESS) { - return false; - } +bool DesktopPlatform::CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) +{ + if (glfwCreateWindowSurface(instance, window, nullptr, surface) != VK_SUCCESS) + { + return false; + } - return true; + return true; } -void DesktopPlatform::SetResizeCallback(std::function callback) { - resizeCallback = std::move(callback); +void DesktopPlatform::SetResizeCallback(std::function callback) +{ + resizeCallback = std::move(callback); } -void DesktopPlatform::SetMouseCallback(std::function callback) { - mouseCallback = std::move(callback); +void DesktopPlatform::SetMouseCallback(std::function callback) +{ + mouseCallback = std::move(callback); } -void DesktopPlatform::SetKeyboardCallback(std::function callback) { - keyboardCallback = std::move(callback); +void DesktopPlatform::SetKeyboardCallback(std::function callback) +{ + keyboardCallback = std::move(callback); } -void DesktopPlatform::SetCharCallback(std::function callback) { - charCallback = std::move(callback); +void DesktopPlatform::SetCharCallback(std::function callback) +{ + charCallback = std::move(callback); } -void DesktopPlatform::SetWindowTitle(const std::string& title) { - if (window) { - glfwSetWindowTitle(window, title.c_str()); - } +void DesktopPlatform::SetWindowTitle(const std::string &title) +{ + if (window) + { + glfwSetWindowTitle(window, title.c_str()); + } } -void DesktopPlatform::WindowResizeCallback(GLFWwindow* window, int width, int height) { - auto* platform = static_cast(glfwGetWindowUserPointer(window)); - platform->width = width; - platform->height = height; - platform->windowResized = true; - - // Call the resize callback if set - if (platform->resizeCallback) { - platform->resizeCallback(width, height); - } +void DesktopPlatform::WindowResizeCallback(GLFWwindow *window, int width, int height) +{ + auto *platform = static_cast(glfwGetWindowUserPointer(window)); + platform->width = width; + platform->height = height; + platform->windowResized = true; + + // Call the resize callback if set + if (platform->resizeCallback) + { + platform->resizeCallback(width, height); + } } -void DesktopPlatform::MousePositionCallback(GLFWwindow* window, double xpos, double ypos) { - auto* platform = static_cast(glfwGetWindowUserPointer(window)); - - // Call the mouse callback if set - if (platform->mouseCallback) { - // Get the mouse button state - uint32_t buttons = 0; - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { - buttons |= 0x01; // Left button - } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { - buttons |= 0x02; // Right button - } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { - buttons |= 0x04; // Middle button - } - - platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); - } +void DesktopPlatform::MousePositionCallback(GLFWwindow *window, double xpos, double ypos) +{ + auto *platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the mouse callback if set + if (platform->mouseCallback) + { + // Get the mouse button state + uint32_t buttons = 0; + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) + { + buttons |= 0x01; // Left button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) + { + buttons |= 0x02; // Right button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) + { + buttons |= 0x04; // Middle button + } + + platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); + } } -void DesktopPlatform::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { - auto* platform = static_cast(glfwGetWindowUserPointer(window)); - - // Call the mouse callback if set - if (platform->mouseCallback) { - // Get the mouse position - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - // Get the mouse button state - uint32_t buttons = 0; - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { - buttons |= 0x01; // Left button - } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) { - buttons |= 0x02; // Right button - } - if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) { - buttons |= 0x04; // Middle button - } - - platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); - } +void DesktopPlatform::MouseButtonCallback(GLFWwindow *window, int button, int action, int mods) +{ + auto *platform = static_cast(glfwGetWindowUserPointer(window)); + + // Call the mouse callback if set + if (platform->mouseCallback) + { + // Get the mouse position + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + + // Get the mouse button state + uint32_t buttons = 0; + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) + { + buttons |= 0x01; // Left button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) + { + buttons |= 0x02; // Right button + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) + { + buttons |= 0x04; // Middle button + } + + platform->mouseCallback(static_cast(xpos), static_cast(ypos), buttons); + } } -void DesktopPlatform::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { - auto* platform = static_cast(glfwGetWindowUserPointer(window)); +void DesktopPlatform::KeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) +{ + auto *platform = static_cast(glfwGetWindowUserPointer(window)); - // Call the keyboard callback if set - if (platform->keyboardCallback) { - platform->keyboardCallback(key, action != GLFW_RELEASE); - } + // Call the keyboard callback if set + if (platform->keyboardCallback) + { + platform->keyboardCallback(key, action != GLFW_RELEASE); + } } -void DesktopPlatform::CharCallback(GLFWwindow* window, unsigned int codepoint) { - auto* platform = static_cast(glfwGetWindowUserPointer(window)); +void DesktopPlatform::CharCallback(GLFWwindow *window, unsigned int codepoint) +{ + auto *platform = static_cast(glfwGetWindowUserPointer(window)); - // Call the char callback if set - if (platform->charCallback) { - platform->charCallback(codepoint); - } + // Call the char callback if set + if (platform->charCallback) + { + platform->charCallback(codepoint); + } } #endif diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h index b8e59a40..5917caa9 100644 --- a/attachments/simple_engine/platform.h +++ b/attachments/simple_engine/platform.h @@ -1,24 +1,46 @@ +/* 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 #if defined(PLATFORM_ANDROID) -#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__)) +# 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__)) #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") +# 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 /** @@ -27,420 +49,458 @@ * This class implements the platform abstraction as described in the Engine_Architecture chapter: * @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc */ -class Platform { -public: - /** - * @brief Default constructor. - */ - Platform() = default; - - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~Platform() = default; - - /** - * @brief Initialize the platform. - * @param appName The name of the application. - * @param width The width of the window. - * @param height The height of the window. - * @return True if initialization was successful, false otherwise. - */ - virtual bool Initialize(const std::string& appName, int width, int height) = 0; - - /** - * @brief Clean up platform resources. - */ - virtual void Cleanup() = 0; - - /** - * @brief Process platform events. - * @return True if the application should continue running, false if it should exit. - */ - virtual bool ProcessEvents() = 0; - - /** - * @brief Check if the window has been resized. - * @return True if the window has been resized, false otherwise. - */ - virtual bool HasWindowResized() = 0; - - /** - * @brief Get the current window width. - * @return The window width. - */ - virtual int GetWindowWidth() const = 0; - - /** - * @brief Get the current window height. - * @return The window height. - */ - virtual int GetWindowHeight() const = 0; - - /** - * @brief Get the current window size. - * @param width Pointer to store the window width. - * @param height Pointer to store the window height. - */ - virtual void GetWindowSize(int* width, int* height) const { - *width = GetWindowWidth(); - *height = GetWindowHeight(); - } - - /** - * @brief Create a Vulkan surface. - * @param instance The Vulkan instance. - * @param surface Pointer to the surface handle to be filled. - * @return True if the surface was created successfully, false otherwise. - */ - virtual bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) = 0; - - /** - * @brief Set a callback for window resize events. - * @param callback The callback function to be called when the window is resized. - */ - virtual void SetResizeCallback(std::function callback) = 0; - - /** - * @brief Set a callback for mouse input events. - * @param callback The callback function to be called when mouse input is received. - */ - virtual void SetMouseCallback(std::function callback) = 0; - - /** - * @brief Set a callback for keyboard input events. - * @param callback The callback function to be called when keyboard input is received. - */ - virtual void SetKeyboardCallback(std::function callback) = 0; - - /** - * @brief Set a callback for character input events. - * @param callback The callback function to be called when character input is received. - */ - virtual void SetCharCallback(std::function callback) = 0; - - /** - * @brief Set the window title. - * @param title The new window title. - */ - virtual void SetWindowTitle(const std::string& title) = 0; +class Platform +{ + public: + /** + * @brief Default constructor. + */ + Platform() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Platform() = default; + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + virtual bool Initialize(const std::string &appName, int width, int height) = 0; + + /** + * @brief Clean up platform resources. + */ + virtual void Cleanup() = 0; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + virtual bool ProcessEvents() = 0; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + virtual bool HasWindowResized() = 0; + + /** + * @brief Get the current window width. + * @return The window width. + */ + virtual int GetWindowWidth() const = 0; + + /** + * @brief Get the current window height. + * @return The window height. + */ + virtual int GetWindowHeight() const = 0; + + /** + * @brief Get the current window size. + * @param width Pointer to store the window width. + * @param height Pointer to store the window height. + */ + virtual void GetWindowSize(int *width, int *height) const + { + *width = GetWindowWidth(); + *height = GetWindowHeight(); + } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + virtual bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) = 0; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + virtual void SetResizeCallback(std::function callback) = 0; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + virtual void SetMouseCallback(std::function callback) = 0; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + virtual void SetKeyboardCallback(std::function callback) = 0; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + virtual void SetCharCallback(std::function callback) = 0; + + /** + * @brief Set the window title. + * @param title The new window title. + */ + virtual void SetWindowTitle(const std::string &title) = 0; }; #if defined(PLATFORM_ANDROID) /** * @brief Android implementation of the Platform interface. */ -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; - - // Mobile-specific properties - 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; - - /** - * @brief Detect device capabilities for performance optimizations. - */ - void DetectDeviceCapabilities(); - - /** - * @brief Set up power-saving mode based on battery level. - */ - void SetupPowerSavingMode(); - - /** - * @brief Initialize touch input handling. - */ - void InitializeTouchInput(); - -public: - /** - * @brief Enable or disable power-saving mode. - * @param enable Whether to enable power-saving mode. - */ - void EnablePowerSavingMode(bool enable); - - /** - * @brief Check if power-saving mode is enabled. - * @return True if power-saving mode is enabled, false otherwise. - */ - bool IsPowerSavingModeEnabled() const { return powerSavingMode; } - - /** - * @brief Enable or disable multi-touch input. - * @param enable Whether to enable multi-touch input. - */ - void EnableMultiTouch(bool enable) { multiTouchEnabled = enable; } - - /** - * @brief Check if multi-touch input is enabled. - * @return True if multi-touch input is enabled, false otherwise. - */ - bool IsMultiTouchEnabled() const { return multiTouchEnabled; } - - /** - * @brief Get the device capabilities. - * @return The device capabilities. - */ - const DeviceCapabilities& GetDeviceCapabilities() const { return deviceCapabilities; } - /** - * @brief Constructor with an Android app. - * @param androidApp The Android app. - */ - explicit AndroidPlatform(android_app* androidApp); - - /** - * @brief Initialize the platform. - * @param appName The name of the application. - * @param width The width of the window. - * @param height The height of the window. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(const std::string& appName, int width, int height) override; - - /** - * @brief Clean up platform resources. - */ - void Cleanup() override; - - /** - * @brief Process platform events. - * @return True if the application should continue running, false if it should exit. - */ - bool ProcessEvents() override; - - /** - * @brief Check if the window has been resized. - * @return True if the window has been resized, false otherwise. - */ - bool HasWindowResized() override; - - /** - * @brief Get the current window width. - * @return The window width. - */ - int GetWindowWidth() const override { return width; } - - /** - * @brief Get the current window height. - * @return The window height. - */ - int GetWindowHeight() const override { return height; } - - /** - * @brief Create a Vulkan surface. - * @param instance The Vulkan instance. - * @param surface Pointer to the surface handle to be filled. - * @return True if the surface was created successfully, false otherwise. - */ - bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; - - /** - * @brief Set a callback for window resize events. - * @param callback The callback function to be called when the window is resized. - */ - void SetResizeCallback(std::function callback) override; - - /** - * @brief Set a callback for mouse input events. - * @param callback The callback function to be called when mouse input is received. - */ - void SetMouseCallback(std::function callback) override; - - /** - * @brief Set a callback for keyboard input events. - * @param callback The callback function to be called when keyboard input is received. - */ - void SetKeyboardCallback(std::function callback) override; - - /** - * @brief Set a callback for character input events. - * @param callback The callback function to be called when character input is received. - */ - void SetCharCallback(std::function callback) override; - - /** - * @brief Set the window title (no-op on Android). - * @param title The new window title. - */ - void SetWindowTitle(const std::string& title) override; - - /** - * @brief Get the Android app. - * @return The Android app. - */ - android_app* GetApp() const { return app; } - - /** - * @brief Get the asset manager. - * @return The asset manager. - */ - AAssetManager* GetAssetManager() const { return app ? app->activity->assetManager : nullptr; } +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; + + // Mobile-specific properties + 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; + + /** + * @brief Detect device capabilities for performance optimizations. + */ + void DetectDeviceCapabilities(); + + /** + * @brief Set up power-saving mode based on battery level. + */ + void SetupPowerSavingMode(); + + /** + * @brief Initialize touch input handling. + */ + void InitializeTouchInput(); + + public: + /** + * @brief Enable or disable power-saving mode. + * @param enable Whether to enable power-saving mode. + */ + void EnablePowerSavingMode(bool enable); + + /** + * @brief Check if power-saving mode is enabled. + * @return True if power-saving mode is enabled, false otherwise. + */ + bool IsPowerSavingModeEnabled() const + { + return powerSavingMode; + } + + /** + * @brief Enable or disable multi-touch input. + * @param enable Whether to enable multi-touch input. + */ + void EnableMultiTouch(bool enable) + { + multiTouchEnabled = enable; + } + + /** + * @brief Check if multi-touch input is enabled. + * @return True if multi-touch input is enabled, false otherwise. + */ + bool IsMultiTouchEnabled() const + { + return multiTouchEnabled; + } + + /** + * @brief Get the device capabilities. + * @return The device capabilities. + */ + const DeviceCapabilities &GetDeviceCapabilities() const + { + return deviceCapabilities; + } + /** + * @brief Constructor with an Android app. + * @param androidApp The Android app. + */ + explicit AndroidPlatform(android_app *androidApp); + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string &appName, int width, int height) override; + + /** + * @brief Clean up platform resources. + */ + void Cleanup() override; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + bool ProcessEvents() override; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + bool HasWindowResized() override; + + /** + * @brief Get the current window width. + * @return The window width. + */ + int GetWindowWidth() const override + { + return width; + } + + /** + * @brief Get the current window height. + * @return The window height. + */ + int GetWindowHeight() const override + { + return height; + } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) override; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + void SetResizeCallback(std::function callback) override; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + void SetMouseCallback(std::function callback) override; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + void SetKeyboardCallback(std::function callback) override; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + void SetCharCallback(std::function callback) override; + + /** + * @brief Set the window title (no-op on Android). + * @param title The new window title. + */ + void SetWindowTitle(const std::string &title) override; + + /** + * @brief Get the Android app. + * @return The Android app. + */ + android_app *GetApp() const + { + return app; + } + + /** + * @brief Get the asset manager. + * @return The asset manager. + */ + AAssetManager *GetAssetManager() const + { + return app ? app->activity->assetManager : nullptr; + } }; #else /** * @brief Desktop implementation of the Platform interface. */ -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; - - /** - * @brief Static callback for GLFW window resize events. - * @param window The GLFW window. - * @param width The new width. - * @param height The new height. - */ - static void WindowResizeCallback(GLFWwindow* window, int width, int height); - - /** - * @brief Static callback for GLFW mouse position events. - * @param window The GLFW window. - * @param xpos The x-coordinate of the cursor. - * @param ypos The y-coordinate of the cursor. - */ - static void MousePositionCallback(GLFWwindow* window, double xpos, double ypos); - - /** - * @brief Static callback for GLFW mouse button events. - * @param window The GLFW window. - * @param button The mouse button that was pressed or released. - * @param action The action (GLFW_PRESS or GLFW_RELEASE). - * @param mods The modifier keys that were held down. - */ - static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods); - - /** - * @brief Static callback for GLFW keyboard events. - * @param window The GLFW window. - * @param key The key that was pressed or released. - * @param scancode The system-specific scancode of the key. - * @param action The action (GLFW_PRESS, GLFW_RELEASE, or GLFW_REPEAT). - * @param mods The modifier keys that were held down. - */ - static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); - - /** - * @brief Static callback for GLFW character events. - * @param window The GLFW window. - * @param codepoint The Unicode code point of the character. - */ - static void CharCallback(GLFWwindow* window, unsigned int codepoint); - -public: - /** - * @brief Default constructor. - */ - DesktopPlatform() = default; - - /** - * @brief Initialize the platform. - * @param appName The name of the application. - * @param width The width of the window. - * @param height The height of the window. - * @return True if initialization was successful, false otherwise. - */ - bool Initialize(const std::string& appName, int width, int height) override; - - /** - * @brief Clean up platform resources. - */ - void Cleanup() override; - - /** - * @brief Process platform events. - * @return True if the application should continue running, false if it should exit. - */ - bool ProcessEvents() override; - - /** - * @brief Check if the window has been resized. - * @return True if the window has been resized, false otherwise. - */ - bool HasWindowResized() override; - - /** - * @brief Get the current window width. - * @return The window width. - */ - int GetWindowWidth() const override { return width; } - - /** - * @brief Get the current window height. - * @return The window height. - */ - int GetWindowHeight() const override { return height; } - - /** - * @brief Create a Vulkan surface. - * @param instance The Vulkan instance. - * @param surface Pointer to the surface handle to be filled. - * @return True if the surface was created successfully, false otherwise. - */ - bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR* surface) override; - - /** - * @brief Set a callback for window resize events. - * @param callback The callback function to be called when the window is resized. - */ - void SetResizeCallback(std::function callback) override; - - /** - * @brief Set a callback for mouse input events. - * @param callback The callback function to be called when mouse input is received. - */ - void SetMouseCallback(std::function callback) override; - - /** - * @brief Set a callback for keyboard input events. - * @param callback The callback function to be called when keyboard input is received. - */ - void SetKeyboardCallback(std::function callback) override; - - /** - * @brief Set a callback for character input events. - * @param callback The callback function to be called when character input is received. - */ - void SetCharCallback(std::function callback) override; - - /** - * @brief Set the window title. - * @param title The new window title. - */ - void SetWindowTitle(const std::string& title) override; - - /** - * @brief Get the GLFW window. - * @return The GLFW window. - */ - GLFWwindow* GetWindow() const { return 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; + + /** + * @brief Static callback for GLFW window resize events. + * @param window The GLFW window. + * @param width The new width. + * @param height The new height. + */ + static void WindowResizeCallback(GLFWwindow *window, int width, int height); + + /** + * @brief Static callback for GLFW mouse position events. + * @param window The GLFW window. + * @param xpos The x-coordinate of the cursor. + * @param ypos The y-coordinate of the cursor. + */ + static void MousePositionCallback(GLFWwindow *window, double xpos, double ypos); + + /** + * @brief Static callback for GLFW mouse button events. + * @param window The GLFW window. + * @param button The mouse button that was pressed or released. + * @param action The action (GLFW_PRESS or GLFW_RELEASE). + * @param mods The modifier keys that were held down. + */ + static void MouseButtonCallback(GLFWwindow *window, int button, int action, int mods); + + /** + * @brief Static callback for GLFW keyboard events. + * @param window The GLFW window. + * @param key The key that was pressed or released. + * @param scancode The system-specific scancode of the key. + * @param action The action (GLFW_PRESS, GLFW_RELEASE, or GLFW_REPEAT). + * @param mods The modifier keys that were held down. + */ + static void KeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); + + /** + * @brief Static callback for GLFW character events. + * @param window The GLFW window. + * @param codepoint The Unicode code point of the character. + */ + static void CharCallback(GLFWwindow *window, unsigned int codepoint); + + public: + /** + * @brief Default constructor. + */ + DesktopPlatform() = default; + + /** + * @brief Initialize the platform. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string &appName, int width, int height) override; + + /** + * @brief Clean up platform resources. + */ + void Cleanup() override; + + /** + * @brief Process platform events. + * @return True if the application should continue running, false if it should exit. + */ + bool ProcessEvents() override; + + /** + * @brief Check if the window has been resized. + * @return True if the window has been resized, false otherwise. + */ + bool HasWindowResized() override; + + /** + * @brief Get the current window width. + * @return The window width. + */ + int GetWindowWidth() const override + { + return width; + } + + /** + * @brief Get the current window height. + * @return The window height. + */ + int GetWindowHeight() const override + { + return height; + } + + /** + * @brief Create a Vulkan surface. + * @param instance The Vulkan instance. + * @param surface Pointer to the surface handle to be filled. + * @return True if the surface was created successfully, false otherwise. + */ + bool CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) override; + + /** + * @brief Set a callback for window resize events. + * @param callback The callback function to be called when the window is resized. + */ + void SetResizeCallback(std::function callback) override; + + /** + * @brief Set a callback for mouse input events. + * @param callback The callback function to be called when mouse input is received. + */ + void SetMouseCallback(std::function callback) override; + + /** + * @brief Set a callback for keyboard input events. + * @param callback The callback function to be called when keyboard input is received. + */ + void SetKeyboardCallback(std::function callback) override; + + /** + * @brief Set a callback for character input events. + * @param callback The callback function to be called when character input is received. + */ + void SetCharCallback(std::function callback) override; + + /** + * @brief Set the window title. + * @param title The new window title. + */ + void SetWindowTitle(const std::string &title) override; + + /** + * @brief Get the GLFW window. + * @return The GLFW window. + */ + GLFWwindow *GetWindow() const + { + return window; + } }; #endif @@ -449,11 +509,12 @@ class DesktopPlatform final : public Platform { * @param args Arguments to pass to the platform constructor. * @return A unique pointer to the platform instance. */ -template -std::unique_ptr CreatePlatform(Args&&... args) { +template +std::unique_ptr CreatePlatform(Args &&...args) +{ #if defined(PLATFORM_ANDROID) - return std::make_unique(std::forward(args)...); + return std::make_unique(std::forward(args)...); #else - return std::make_unique(); + return std::make_unique(); #endif } diff --git a/attachments/simple_engine/renderdoc_debug_system.cpp b/attachments/simple_engine/renderdoc_debug_system.cpp index 9cd23ad9..dccddf72 100644 --- a/attachments/simple_engine/renderdoc_debug_system.cpp +++ b/attachments/simple_engine/renderdoc_debug_system.cpp @@ -1,121 +1,162 @@ +/* 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 "renderdoc_debug_system.h" #include #include #if defined(_WIN32) - #define WIN32_LEAN_AND_MEAN - #include +# define WIN32_LEAN_AND_MEAN +# include #elif defined(__APPLE__) || defined(__linux__) - #include +# include #endif // Value for eRENDERDOC_API_Version_1_4_1 from RenderDoc's header to avoid including it #ifndef RENDERDOC_API_VERSION_1_4_1 -#define RENDERDOC_API_VERSION_1_4_1 10401 +# define RENDERDOC_API_VERSION_1_4_1 10401 #endif // Minimal local typedefs and struct to receive function pointers without including renderdoc_app.h -using pTriggerCaptureLocal = void (*)(); -using pStartFrameCaptureLocal = void (*)(void*, void*); -using pEndFrameCaptureLocal = unsigned int (*)(void*, void*); - -struct RENDERDOC_API_1_4_1_MIN { - pTriggerCaptureLocal TriggerCapture; - void* _pad0; // We don't rely on layout beyond the subset we read via memcpy - pStartFrameCaptureLocal StartFrameCapture; - pEndFrameCaptureLocal EndFrameCapture; +using pTriggerCaptureLocal = void (*)(); +using pStartFrameCaptureLocal = void (*)(void *, void *); +using pEndFrameCaptureLocal = unsigned int (*)(void *, void *); + +struct RENDERDOC_API_1_4_1_MIN +{ + pTriggerCaptureLocal TriggerCapture; + void *_pad0; // We don't rely on layout beyond the subset we read via memcpy + pStartFrameCaptureLocal StartFrameCapture; + pEndFrameCaptureLocal EndFrameCapture; }; -bool RenderDocDebugSystem::LoadRenderDocAPI() { - if (renderdocAvailable) return true; +bool RenderDocDebugSystem::LoadRenderDocAPI() +{ + if (renderdocAvailable) + return true; - // Try to fetch RENDERDOC_GetAPI from a loaded module without forcing a dependency - pRENDERDOC_GetAPI getAPI = nullptr; + // Try to fetch RENDERDOC_GetAPI from a loaded module without forcing a dependency + pRENDERDOC_GetAPI getAPI = nullptr; #if defined(_WIN32) - HMODULE mod = GetModuleHandleA("renderdoc.dll"); - if (!mod) { - // If not already injected/loaded, do not force-load. We can attempt LoadLibraryA as a fallback - mod = LoadLibraryA("renderdoc.dll"); - if (!mod) { - LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); - return false; - } - } - getAPI = reinterpret_cast(GetProcAddress(mod, "RENDERDOC_GetAPI")); + HMODULE mod = GetModuleHandleA("renderdoc.dll"); + if (!mod) + { + // If not already injected/loaded, do not force-load. We can attempt LoadLibraryA as a fallback + mod = LoadLibraryA("renderdoc.dll"); + if (!mod) + { + LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); + return false; + } + } + getAPI = reinterpret_cast(GetProcAddress(mod, "RENDERDOC_GetAPI")); #elif defined(__APPLE__) || defined(__linux__) - void* mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); - if (!mod) { - // Try to load if not already loaded; if unavailable, just no-op - mod = dlopen("librenderdoc.so", RTLD_NOW); - if (!mod) { - LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); - return false; - } - } - getAPI = reinterpret_cast(dlsym(mod, "RENDERDOC_GetAPI")); + void *mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + if (!mod) + { + // Try to load if not already loaded; if unavailable, just no-op + mod = dlopen("librenderdoc.so", RTLD_NOW); + if (!mod) + { + LOG_INFO("RenderDoc", "RenderDoc not loaded into process"); + return false; + } + } + getAPI = reinterpret_cast(dlsym(mod, "RENDERDOC_GetAPI")); #endif - if (!getAPI) { - LOG_WARNING("RenderDoc", "RENDERDOC_GetAPI symbol not found"); - return false; - } - - // Request API 1.4.1 into a temporary buffer and then extract needed functions - RENDERDOC_API_1_4_1_MIN apiMin{}; - void* apiPtr = nullptr; - int result = getAPI(RENDERDOC_API_VERSION_1_4_1, &apiPtr); - if (result == 0 || apiPtr == nullptr) { - LOG_WARNING("RenderDoc", "Failed to acquire RenderDoc API 1.4.1"); - return false; - } - - // Copy only the subset we care about; layout is stable for these early members - std::memcpy(&apiMin, apiPtr, sizeof(apiMin)); - - fnTriggerCapture = apiMin.TriggerCapture; - fnStartFrameCapture = apiMin.StartFrameCapture; - fnEndFrameCapture = apiMin.EndFrameCapture; - - renderdocAvailable = (fnTriggerCapture || fnStartFrameCapture || fnEndFrameCapture); - - if (renderdocAvailable) { - LOG_INFO("RenderDoc", "RenderDoc API loaded"); - } else { - LOG_WARNING("RenderDoc", "RenderDoc API did not provide expected functions"); - } - - return renderdocAvailable; + if (!getAPI) + { + LOG_WARNING("RenderDoc", "RENDERDOC_GetAPI symbol not found"); + return false; + } + + // Request API 1.4.1 into a temporary buffer and then extract needed functions + RENDERDOC_API_1_4_1_MIN apiMin{}; + void *apiPtr = nullptr; + int result = getAPI(RENDERDOC_API_VERSION_1_4_1, &apiPtr); + if (result == 0 || apiPtr == nullptr) + { + LOG_WARNING("RenderDoc", "Failed to acquire RenderDoc API 1.4.1"); + return false; + } + + // Copy only the subset we care about; layout is stable for these early members + std::memcpy(&apiMin, apiPtr, sizeof(apiMin)); + + fnTriggerCapture = apiMin.TriggerCapture; + fnStartFrameCapture = apiMin.StartFrameCapture; + fnEndFrameCapture = apiMin.EndFrameCapture; + + renderdocAvailable = (fnTriggerCapture || fnStartFrameCapture || fnEndFrameCapture); + + if (renderdocAvailable) + { + LOG_INFO("RenderDoc", "RenderDoc API loaded"); + } + else + { + LOG_WARNING("RenderDoc", "RenderDoc API did not provide expected functions"); + } + + return renderdocAvailable; } -void RenderDocDebugSystem::TriggerCapture() { - if (!renderdocAvailable && !LoadRenderDocAPI()) return; - if (fnTriggerCapture) { - fnTriggerCapture(); - LOG_INFO("RenderDoc", "Triggered capture"); - } else { - LOG_WARNING("RenderDoc", "TriggerCapture not available"); - } +void RenderDocDebugSystem::TriggerCapture() +{ + if (!renderdocAvailable && !LoadRenderDocAPI()) + return; + if (fnTriggerCapture) + { + fnTriggerCapture(); + LOG_INFO("RenderDoc", "Triggered capture"); + } + else + { + LOG_WARNING("RenderDoc", "TriggerCapture not available"); + } } -void RenderDocDebugSystem::StartFrameCapture(void* device, void* window) { - if (!renderdocAvailable && !LoadRenderDocAPI()) return; - if (fnStartFrameCapture) { - fnStartFrameCapture(device, window); - LOG_DEBUG("RenderDoc", "StartFrameCapture called"); - } else { - LOG_WARNING("RenderDoc", "StartFrameCapture not available"); - } +void RenderDocDebugSystem::StartFrameCapture(void *device, void *window) +{ + if (!renderdocAvailable && !LoadRenderDocAPI()) + return; + if (fnStartFrameCapture) + { + fnStartFrameCapture(device, window); + LOG_DEBUG("RenderDoc", "StartFrameCapture called"); + } + else + { + LOG_WARNING("RenderDoc", "StartFrameCapture not available"); + } } -bool RenderDocDebugSystem::EndFrameCapture(void* device, void* window) { - if (!renderdocAvailable && !LoadRenderDocAPI()) return false; - if (fnEndFrameCapture) { - unsigned int ok = fnEndFrameCapture(device, window); - LOG_DEBUG("RenderDoc", ok ? "EndFrameCapture succeeded" : "EndFrameCapture failed"); - return ok != 0; - } - LOG_WARNING("RenderDoc", "EndFrameCapture not available"); - return false; +bool RenderDocDebugSystem::EndFrameCapture(void *device, void *window) +{ + if (!renderdocAvailable && !LoadRenderDocAPI()) + return false; + if (fnEndFrameCapture) + { + unsigned int ok = fnEndFrameCapture(device, window); + LOG_DEBUG("RenderDoc", ok ? "EndFrameCapture succeeded" : "EndFrameCapture failed"); + return ok != 0; + } + LOG_WARNING("RenderDoc", "EndFrameCapture not available"); + return false; } diff --git a/attachments/simple_engine/renderdoc_debug_system.h b/attachments/simple_engine/renderdoc_debug_system.h index fc1bbce4..510231ef 100644 --- a/attachments/simple_engine/renderdoc_debug_system.h +++ b/attachments/simple_engine/renderdoc_debug_system.h @@ -1,3 +1,19 @@ +/* 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 "debug_system.h" @@ -6,49 +22,54 @@ // This header intentionally does NOT include to avoid a hard dependency. // Instead, we declare a minimal interface and dynamically resolve the API if present. -class RenderDocDebugSystem final : public DebugSystem { -public: - static RenderDocDebugSystem& GetInstance() { - static RenderDocDebugSystem instance; - return instance; - } +class RenderDocDebugSystem final : public DebugSystem +{ + public: + static RenderDocDebugSystem &GetInstance() + { + static RenderDocDebugSystem instance; + return instance; + } - // Attempt to load the RenderDoc API from the current process. - // Safe to call multiple times. - bool LoadRenderDocAPI(); + // Attempt to load the RenderDoc API from the current process. + // Safe to call multiple times. + bool LoadRenderDocAPI(); - // Returns true if the RenderDoc API has been successfully loaded. - bool IsAvailable() const { return renderdocAvailable; } + // Returns true if the RenderDoc API has been successfully loaded. + bool IsAvailable() const + { + return renderdocAvailable; + } - // Triggers an immediate capture (equivalent to pressing the capture hotkey in the UI). - void TriggerCapture(); + // Triggers an immediate capture (equivalent to pressing the capture hotkey in the UI). + void TriggerCapture(); - // Starts a frame capture for the given device/window (can be nullptr to auto-detect on many backends). - void StartFrameCapture(void* device = nullptr, void* window = nullptr); + // Starts a frame capture for the given device/window (can be nullptr to auto-detect on many backends). + void StartFrameCapture(void *device = nullptr, void *window = nullptr); - // Ends a frame capture previously started. Returns true on success. - bool EndFrameCapture(void* device = nullptr, void* window = nullptr); + // Ends a frame capture previously started. Returns true on success. + bool EndFrameCapture(void *device = nullptr, void *window = nullptr); -private: - RenderDocDebugSystem() = default; - ~RenderDocDebugSystem() override = default; + private: + RenderDocDebugSystem() = default; + ~RenderDocDebugSystem() override = default; - RenderDocDebugSystem(const RenderDocDebugSystem&) = delete; - RenderDocDebugSystem& operator=(const RenderDocDebugSystem&) = delete; + RenderDocDebugSystem(const RenderDocDebugSystem &) = delete; + RenderDocDebugSystem &operator=(const RenderDocDebugSystem &) = delete; - // Internal function pointers matching the subset of RenderDoc API we use. - // We avoid including the official header by declaring minimal signatures. - using pRENDERDOC_GetAPI = int (*)(int, void**); + // Internal function pointers matching the subset of RenderDoc API we use. + // We avoid including the official header by declaring minimal signatures. + using pRENDERDOC_GetAPI = int (*)(int, void **); - // Subset of API function pointers - typedef void (*pRENDERDOC_TriggerCapture)(); - typedef void (*pRENDERDOC_StartFrameCapture)(void* device, void* window); - typedef unsigned int (*pRENDERDOC_EndFrameCapture)(void* device, void* window); // returns bool in C API + // Subset of API function pointers + typedef void (*pRENDERDOC_TriggerCapture)(); + typedef void (*pRENDERDOC_StartFrameCapture)(void *device, void *window); + typedef unsigned int (*pRENDERDOC_EndFrameCapture)(void *device, void *window); // returns bool in C API - // Storage for resolved API - pRENDERDOC_TriggerCapture fnTriggerCapture = nullptr; - pRENDERDOC_StartFrameCapture fnStartFrameCapture = nullptr; - pRENDERDOC_EndFrameCapture fnEndFrameCapture = nullptr; + // Storage for resolved API + pRENDERDOC_TriggerCapture fnTriggerCapture = nullptr; + pRENDERDOC_StartFrameCapture fnStartFrameCapture = nullptr; + pRENDERDOC_EndFrameCapture fnEndFrameCapture = nullptr; - bool renderdocAvailable = false; + bool renderdocAvailable = false; }; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index df3e548e..a3f3387f 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -1,111 +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. + */ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include +#include +#include +#include +#include -#include "platform.h" -#include "entity.h" -#include "mesh_component.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(); - } +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; +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(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 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(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 }; /** * @brief Structure for the uniform buffer object (now without fixed light arrays). */ -struct UniformBufferObject { - 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; - 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; +struct UniformBufferObject +{ + 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; + 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; // reserved }; +static_assert(sizeof(RayQueryUniformBufferObject) == 272, "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); /** * @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; +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 }; /** @@ -114,759 +236,1472 @@ struct MaterialProperties { * 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); - - /** - * @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); - - /** - * @brief Wait for the device to be idle. - */ - void WaitIdle(); - - /** - * @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; } - - // 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(); } - - // 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). - bool IsLoading() const { return loadingFlag.load() || criticalJobsOutstanding.load() > 0; } - void SetLoading(bool v) { loadingFlag.store(v); } - - // 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) const { - // 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; - } - - /** - * @brief Set static lights loaded during model initialization. - * @param lights The lights to store statically. - */ - void SetStaticLights(const std::vector& lights) { staticLights = lights; } - - /** - * @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; - } - - /** - * @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); - - /** - * @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. - */ - void updateAllDescriptorSetsWithNewLightBuffers(); - - // 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); - - 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); - - // 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); - -private: - // 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) - - // 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; - - // 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; - // 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; - - // Pipeline rendering create info structures (for proper lifetime management) - vk::PipelineRenderingCreateInfo mainPipelineRenderingCreateInfo; - vk::PipelineRenderingCreateInfo pbrPipelineRenderingCreateInfo; - vk::PipelineRenderingCreateInfo lightingPipelineRenderingCreateInfo; - - // 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; - - // 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; - - // 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 - vk::raii::Image opaqueSceneColorImage{nullptr}; - vk::raii::DeviceMemory opaqueSceneColorImageMemory{nullptr}; // <-- Standard Vulkan memory - vk::raii::ImageView opaqueSceneColorImageView{nullptr}; - vk::raii::Sampler opaqueSceneColorSampler{nullptr}; - - // A descriptor set for the opaque scene color texture. We will have one for each frame in flight - // to match the swapchain images. - std::vector transparentDescriptorSets; - // Fallback descriptor sets for opaque pass (binds a default SHADER_READ_ONLY texture as Set 1) - std::vector transparentFallbackDescriptorSets; - - // 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; - }; - 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::vector pendingTextureJobs; - // Track outstanding critical texture jobs (for IsLoading) - std::atomic criticalJobsOutstanding{0}; - - // 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}; - - // 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; - - // 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}; - - // 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; - }; - 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_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME - }; - - // All device extensions (required + optional) - std::vector deviceExtensions; - - // Initialization flag - bool initialized = false; - - // Framebuffer resized flag (atomic to handle platform callback vs. render thread) - std::atomic framebufferResized{false}; - - // Maximum number of frames in flight - const uint32_t MAX_FRAMES_IN_FLIGHT = 2u; - - // 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 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 createCommandBuffers(); - bool createSyncObjects(); - - void cleanupSwapChain(); - - // Ensure Vulkan-Hpp dispatcher is initialized for the current thread when using RAII objects on worker threads - void ensureThreadLocalVulkanInit() const; - void recreateSwapChain(); - - void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera); - void updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera, const glm::mat4& customTransform); - void updateUniformBufferInternal(uint32_t currentImage, Entity* entity, CameraComponent* camera, UniformBufferObject& ubo); - - 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); - 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); - void transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels = 1); - void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector& regions) const; - - vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 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); -}; +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); + + /** + * @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); + + /** + * @brief Wait for the device to be idle. + */ + void WaitIdle(); + + /** + * @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; + } + + // 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(); + } + + // 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. + bool IsLoading() const + { + return (loadingFlag.load() || criticalJobsOutstanding.load() > 0) && !initialLoadComplete.load(); + } + void SetLoading(bool v) + { + loadingFlag.store(v); + if (!v) + { + // Mark initial load complete; non-critical streaming can continue in background. + initialLoadComplete.store(true); + } + else + { + // New load cycle starting + initialLoadComplete.store(false); + } + } + + // Descriptor set deferred update machinery + void MarkEntityDescriptorsDirty(Entity *entity); + void ProcessDirtyDescriptorsForFrame(uint32_t frameIndex); + bool updateDescriptorSetsForFrame(Entity *entity, + const std::string &texturePath, + bool usePBR, + uint32_t frameIndex, + bool imagesOnly = false, + bool uboOnly = false); + + // 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; + } + + /** + * @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() + { + asBuildRequested.store(true, std::memory_order_release); + } + // Overload with reason tracking for diagnostics + void RequestAccelerationStructureBuild(const char *reason) + { + if (reason) + lastASBuildRequestReason = reason; + else + lastASBuildRequestReason = "(no reason)"; + 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); + + /** + * @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); + + /** + * @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 ®ions, + 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); + + /** + * @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); + + private: + // 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 + + // Ray Query tuning + int rayQueryMaxBounces = 1; // 0 = no secondary rays, 1 = one-bounce reflection/refraction + // 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; + // 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; + + // 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 + + // 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(); + + // 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 + vk::raii::Image opaqueSceneColorImage{nullptr}; + vk::raii::DeviceMemory opaqueSceneColorImageMemory{nullptr}; // <-- Standard Vulkan memory + vk::raii::ImageView opaqueSceneColorImageView{nullptr}; + vk::raii::Sampler opaqueSceneColorSampler{nullptr}; + + // A descriptor set for the opaque scene color texture. We will have one for each frame in flight + // to match the swapchain images. + 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}; + + // 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; + std::unordered_set 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}; + + // 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. + // This lets us avoid re-writing descriptor binding 0 every frame and prevents + // update-after-bind warnings while keeping initialization correct when a frame + // first becomes current. + std::vector uboBindingWritten; // 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 + }; + 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_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_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}; + + // 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; + size_t lastASBuiltInstanceCount = 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 + const uint32_t MAX_FRAMES_IN_FLIGHT = 1u; + + // --- Performance & diagnostics --- + // CPU-side frustum culling toggle and last-frame stats + 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; + std::thread watchdogThread; + std::atomic watchdogRunning{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); + // 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> &entities); + + // Ensure Vulkan-Hpp dispatcher is initialized for the current thread when using RAII objects on worker threads + void ensureThreadLocalVulkanInit() const; + + // ===================== 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, CameraComponent *camera); + void updateUniformBuffer(uint32_t currentImage, Entity *entity, CameraComponent *camera, const glm::mat4 &customTransform); + void updateUniformBufferInternal(uint32_t currentImage, Entity *entity, CameraComponent *camera, UniformBufferObject &ubo); + + 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); + 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, 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); + void copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector ®ions); + // Extended: track stagedBytes for perf stats + void uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + const std::vector ®ions, + uint32_t mipLevels, + vk::DeviceSize stagedBytes); + + vk::raii::ImageView createImageView(vk::raii::Image &image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels = 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; + } +}; \ No newline at end of file diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp index 65256065..5a645b4d 100644 --- a/attachments/simple_engine/renderer_compute.cpp +++ b/attachments/simple_engine/renderer_compute.cpp @@ -1,263 +1,591 @@ +/* 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 // This file contains compute-related methods from the Renderer class // Create compute pipeline -bool Renderer::createComputePipeline() { - try { - // Read compute shader code - auto computeShaderCode = readFile("shaders/hrtf.spv"); - - // Create shader module - vk::raii::ShaderModule computeShaderModule = createShaderModule(computeShaderCode); - - // Create shader stage info - vk::PipelineShaderStageCreateInfo computeShaderStageInfo{ - .stage = vk::ShaderStageFlagBits::eCompute, - .module = *computeShaderModule, - .pName = "main" - }; - - // Create compute descriptor set layout - std::array computeBindings = { - vk::DescriptorSetLayoutBinding{ - .binding = 0, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eCompute, - .pImmutableSamplers = nullptr - }, - vk::DescriptorSetLayoutBinding{ - .binding = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eCompute, - .pImmutableSamplers = nullptr - }, - vk::DescriptorSetLayoutBinding{ - .binding = 2, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eCompute, - .pImmutableSamplers = nullptr - }, - vk::DescriptorSetLayoutBinding{ - .binding = 3, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .descriptorCount = 1, - .stageFlags = vk::ShaderStageFlagBits::eCompute, - .pImmutableSamplers = nullptr - } - }; - - vk::DescriptorSetLayoutCreateInfo computeLayoutInfo{ - .bindingCount = static_cast(computeBindings.size()), - .pBindings = computeBindings.data() - }; - - computeDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, computeLayoutInfo); - - // Create compute pipeline layout - vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ - .setLayoutCount = 1, - .pSetLayouts = &*computeDescriptorSetLayout, - .pushConstantRangeCount = 0, - .pPushConstantRanges = nullptr - }; - - computePipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); - - // Create compute pipeline - vk::ComputePipelineCreateInfo pipelineInfo{ - .stage = computeShaderStageInfo, - .layout = *computePipelineLayout - }; - - computePipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); - - // Create compute descriptor pool - std::array poolSizes = { - vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eStorageBuffer, - .descriptorCount = 3u * MAX_FRAMES_IN_FLIGHT - }, - vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eUniformBuffer, - .descriptorCount = 1u * MAX_FRAMES_IN_FLIGHT - } - }; - - vk::DescriptorPoolCreateInfo poolInfo{ - .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = MAX_FRAMES_IN_FLIGHT, - .poolSizeCount = static_cast(poolSizes.size()), - .pPoolSizes = poolSizes.data() - }; - - computeDescriptorPool = vk::raii::DescriptorPool(device, poolInfo); - - return createComputeCommandPool(); - } catch (const std::exception& e) { - std::cerr << "Failed to create compute pipeline: " << e.what() << std::endl; - return false; - } +bool Renderer::createComputePipeline() +{ + try + { + // Read compute shader code + auto computeShaderCode = readFile("shaders/hrtf.spv"); + + // Create shader module + vk::raii::ShaderModule computeShaderModule = createShaderModule(computeShaderCode); + + // Create shader stage info + vk::PipelineShaderStageCreateInfo computeShaderStageInfo{ + .stage = vk::ShaderStageFlagBits::eCompute, + .module = *computeShaderModule, + .pName = "main"}; + + // Create compute descriptor set layout + std::array computeBindings = { + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr}, + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr}, + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr}, + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .pImmutableSamplers = nullptr}}; + + vk::DescriptorSetLayoutCreateInfo computeLayoutInfo{ + .bindingCount = static_cast(computeBindings.size()), + .pBindings = computeBindings.data()}; + + computeDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, computeLayoutInfo); + + // Create compute pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*computeDescriptorSetLayout, + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr}; + + computePipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Create compute pipeline + vk::ComputePipelineCreateInfo pipelineInfo{ + .stage = computeShaderStageInfo, + .layout = *computePipelineLayout}; + + computePipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + + // Create compute descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 6u * MAX_FRAMES_IN_FLIGHT // room for multiple compute pipelines + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 2u * MAX_FRAMES_IN_FLIGHT}}; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = 2u * MAX_FRAMES_IN_FLIGHT, + .poolSizeCount = static_cast(poolSizes.size()), + .pPoolSizes = poolSizes.data()}; + + computeDescriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + return createComputeCommandPool(); + } + catch (const std::exception &e) + { + std::cerr << "Failed to create compute pipeline: " << e.what() << std::endl; + return false; + } +} + +// Forward+ compute (tiled light culling) +bool Renderer::createForwardPlusPipelinesAndResources() +{ + try + { + // Load compute shader + auto cullSpv = readFile("shaders/forward_plus_cull.spv"); + vk::raii::ShaderModule cullModule = createShaderModule(cullSpv); + + // Descriptor set layout: 0=lights SSBO (RO), 1=tile headers SSBO (RW), 2=tile indices SSBO (RW), 3=params UBO (RO) + std::array bindings = { + vk::DescriptorSetLayoutBinding{.binding = 0, .descriptorType = vk::DescriptorType::eStorageBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eCompute}, + vk::DescriptorSetLayoutBinding{.binding = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eCompute}, + vk::DescriptorSetLayoutBinding{.binding = 2, .descriptorType = vk::DescriptorType::eStorageBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eCompute}, + vk::DescriptorSetLayoutBinding{.binding = 3, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eCompute}}; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{.bindingCount = static_cast(bindings.size()), .pBindings = bindings.data()}; + forwardPlusDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + // Pipeline layout + vk::PipelineLayoutCreateInfo plInfo{.setLayoutCount = 1, .pSetLayouts = &*forwardPlusDescriptorSetLayout}; + forwardPlusPipelineLayout = vk::raii::PipelineLayout(device, plInfo); + + // Pipeline + vk::PipelineShaderStageCreateInfo stage{.stage = vk::ShaderStageFlagBits::eCompute, .module = *cullModule, .pName = "main"}; + vk::ComputePipelineCreateInfo cpInfo{.stage = stage, .layout = *forwardPlusPipelineLayout}; + forwardPlusPipeline = vk::raii::Pipeline(device, nullptr, cpInfo); + + // Allocate per-frame structs + forwardPlusPerFrame.resize(MAX_FRAMES_IN_FLIGHT); + + // Allocate compute descriptor sets (reuse computeDescriptorPool) + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *forwardPlusDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{.descriptorPool = *computeDescriptorPool, .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, .pSetLayouts = layouts.data()}; + auto sets = vk::raii::DescriptorSets(device, allocInfo); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + { + forwardPlusPerFrame[i].computeSet = std::move(sets[i]); + } + + // Initial buffer allocation based on current swapchain extent (also updates descriptors) + uint32_t tilesX = (swapChainExtent.width + forwardPlusTileSizeX - 1) / forwardPlusTileSizeX; + uint32_t tilesY = (swapChainExtent.height + forwardPlusTileSizeY - 1) / forwardPlusTileSizeY; + if (!createOrResizeForwardPlusBuffers(tilesX, tilesY, forwardPlusSlicesZ)) + { + return false; + } + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create Forward+ compute resources: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::createOrResizeForwardPlusBuffers(uint32_t tilesX, uint32_t tilesY, uint32_t slicesZ, bool updateOnlyCurrentFrame) +{ + try + { + size_t clusters = static_cast(tilesX) * static_cast(tilesY) * static_cast(slicesZ); + size_t indices = clusters * static_cast(MAX_LIGHTS_PER_TILE); + + // Range of frames to touch this call + size_t beginFrame = 0; + size_t endFrame = MAX_FRAMES_IN_FLIGHT; + if (updateOnlyCurrentFrame) + { + beginFrame = static_cast(currentFrame); + endFrame = beginFrame + 1; + } + + for (size_t i = beginFrame; i < endFrame; ++i) + { + auto &f = forwardPlusPerFrame[i]; + bool needTiles = (f.tilesCapacity < clusters) || (f.tileHeaders == nullptr); + bool needIdx = (f.indicesCapacity < indices) || (f.tileLightIndices == nullptr); + + if (needTiles) + { + if (!(f.tileHeaders == nullptr)) + { + f.tileHeaders = nullptr; + f.tileHeadersAlloc.reset(); + } + auto [buf, alloc] = createBufferPooled(clusters * sizeof(TileHeader), vk::BufferUsageFlagBits::eStorageBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileHeaders = std::move(buf); + f.tileHeadersAlloc = std::move(alloc); + f.tilesCapacity = clusters; + // Initialize headers to zero so that count==0 when Forward+ is disabled or before first dispatch + if (f.tileHeadersAlloc && f.tileHeadersAlloc->mappedPtr) + { + std::memset(f.tileHeadersAlloc->mappedPtr, 0, clusters * sizeof(TileHeader)); + } + } + if (needIdx) + { + if (!(f.tileLightIndices == nullptr)) + { + f.tileLightIndices = nullptr; + f.tileLightIndicesAlloc.reset(); + } + auto [buf, alloc] = createBufferPooled(indices * sizeof(uint32_t), vk::BufferUsageFlagBits::eStorageBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.tileLightIndices = std::move(buf); + f.tileLightIndicesAlloc = std::move(alloc); + f.indicesCapacity = indices; + // Initialize indices to zero to avoid stray reads + if (f.tileLightIndicesAlloc && f.tileLightIndicesAlloc->mappedPtr) + { + std::memset(f.tileLightIndicesAlloc->mappedPtr, 0, indices * sizeof(uint32_t)); + } + } + if (f.params == nullptr) + { + auto [pbuf, palloc] = createBufferPooled(sizeof(glm::mat4) * 2 + sizeof(glm::vec4) * 3, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + f.params = std::move(pbuf); + f.paramsAlloc = std::move(palloc); + f.paramsMapped = f.paramsAlloc->mappedPtr; + } + + // Update compute descriptor set writes for this frame (only if buffers changed or first time) + if (!(forwardPlusPerFrame[i].computeSet == nullptr)) + { + if (!descriptorSetsValid.load(std::memory_order_relaxed)) + { + // Descriptor sets are being recreated; skip writes this iteration + continue; + } + if (isRecordingCmd.load(std::memory_order_relaxed)) + { + // Avoid update-after-bind while a command buffer is recording + continue; + } + // Only update descriptors if we resized or created any buffer this iteration + if (needTiles || needIdx || f.params != nullptr) + { + // Build writes conditionally to avoid dereferencing uninitialized light buffers + std::vector writes; + + // Binding 0: lights SSBO (only if available) + bool haveLightBuffer = (i < lightStorageBuffers.size()) && !(lightStorageBuffers[i].buffer == nullptr); + vk::DescriptorBufferInfo lightsInfo{}; + if (haveLightBuffer) + { + lightsInfo = vk::DescriptorBufferInfo{.buffer = *lightStorageBuffers[i].buffer, .offset = 0, .range = VK_WHOLE_SIZE}; + writes.push_back(vk::WriteDescriptorSet{.dstSet = *forwardPlusPerFrame[i].computeSet, .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightsInfo}); + } + + // Binding 1: tile headers + vk::DescriptorBufferInfo headersInfo{.buffer = *f.tileHeaders, .offset = 0, .range = VK_WHOLE_SIZE}; + writes.push_back(vk::WriteDescriptorSet{.dstSet = *forwardPlusPerFrame[i].computeSet, .dstBinding = 1, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &headersInfo}); + + // Binding 2: tile indices + vk::DescriptorBufferInfo indicesInfo{.buffer = *f.tileLightIndices, .offset = 0, .range = VK_WHOLE_SIZE}; + writes.push_back(vk::WriteDescriptorSet{.dstSet = *forwardPlusPerFrame[i].computeSet, .dstBinding = 2, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &indicesInfo}); + + // Binding 3: params UBO + vk::DescriptorBufferInfo paramsInfo{.buffer = *f.params, .offset = 0, .range = VK_WHOLE_SIZE}; + writes.push_back(vk::WriteDescriptorSet{.dstSet = *forwardPlusPerFrame[i].computeSet, .dstBinding = 3, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = ¶msInfo}); + + if (!writes.empty()) + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + } + } + } + + // Update PBR descriptor sets to bind new tile buffers for forward shading. + // Avoid updating sets that may be in use by in-flight command buffers. + // If updateOnlyCurrentFrame=true, only update the current frame's sets (safe point after fence wait). + try + { + // Only update PBR descriptor sets for bindings 7/8 in two situations: + // - When called in initialization/device-idle paths (updateOnlyCurrentFrame=false), or + // - When this call resulted in (re)creating the buffers for the current frame + size_t beginFrameSets = 0; + size_t endFrameSets = forwardPlusPerFrame.size(); + if (updateOnlyCurrentFrame) + { + beginFrameSets = static_cast(currentFrame); + endFrameSets = beginFrameSets + 1; + } + + for (auto &kv : entityResources) + { + auto &resources = kv.second; + if (resources.pbrDescriptorSets.empty()) + continue; + for (size_t i = beginFrameSets; i < endFrameSets && i < resources.pbrDescriptorSets.size() && i < forwardPlusPerFrame.size(); ++i) + { + if (!descriptorSetsValid.load(std::memory_order_relaxed)) + continue; + if (isRecordingCmd.load(std::memory_order_relaxed)) + continue; + if (!(*resources.pbrDescriptorSets[i])) + continue; + auto &f = forwardPlusPerFrame[i]; + if ((f.tileHeaders == nullptr) || (f.tileLightIndices == nullptr)) + continue; + vk::DescriptorBufferInfo headersInfo{.buffer = *f.tileHeaders, .offset = 0, .range = VK_WHOLE_SIZE}; + vk::DescriptorBufferInfo indicesInfo{.buffer = *f.tileLightIndices, .offset = 0, .range = VK_WHOLE_SIZE}; + std::array writes = { + vk::WriteDescriptorSet{.dstSet = *resources.pbrDescriptorSets[i], .dstBinding = 7, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &headersInfo}, + vk::WriteDescriptorSet{.dstSet = *resources.pbrDescriptorSets[i], .dstBinding = 8, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &indicesInfo}}; + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + } + } + } + catch (...) + {} + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create/resize Forward+ buffers: " << e.what() << std::endl; + return false; + } +} + +void Renderer::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) +{ + if (frameIndex >= forwardPlusPerFrame.size()) + return; + auto &f = forwardPlusPerFrame[frameIndex]; + if (!f.paramsMapped) + return; + + // Pack: [view][proj][screen xy, tile xy][lightCount, maxPerTile, tilesX, tilesY][near, far, slicesZ, 0] + struct ParamsCPU + { + glm::mat4 view; + glm::mat4 proj; + glm::vec4 screenTile; // x=width,y=height,z=tileX,w=tileY + glm::uvec4 counts; // x=lightCount,y=maxPerTile,z=tilesX,w=tilesY + glm::vec4 zParams; // x=nearZ,y=farZ,z=slicesZ,w=0 + }; + + ParamsCPU p{}; + p.view = view; + p.proj = proj; + p.screenTile = glm::vec4(static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), static_cast(forwardPlusTileSizeX), static_cast(forwardPlusTileSizeY)); + p.counts = glm::uvec4(lightCount, MAX_LIGHTS_PER_TILE, tilesX, tilesY); + p.zParams = glm::vec4(nearZ, farZ, static_cast(slicesZ), 0.0f); + + std::memcpy(f.paramsAlloc->mappedPtr, &p, sizeof(ParamsCPU)); +} + +void Renderer::dispatchForwardPlus(vk::raii::CommandBuffer &cmd, uint32_t tilesX, uint32_t tilesY, uint32_t slicesZ) +{ + if (forwardPlusPipeline == nullptr) + return; + if (currentFrame >= forwardPlusPerFrame.size()) + return; + auto &f = forwardPlusPerFrame[currentFrame]; + if (f.computeSet == nullptr) + return; + + // Ensure a valid lights buffer is bound; otherwise skip compute this frame + bool haveLightBuffer = (currentFrame < lightStorageBuffers.size()) && !(lightStorageBuffers[currentFrame].buffer == nullptr); + if (!haveLightBuffer) + return; + + cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *forwardPlusPipeline); + vk::DescriptorSet set = *f.computeSet; + cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *forwardPlusPipelineLayout, 0, set, {}); + // One invocation per cluster (X,Y by workgroup grid, Z as third dimension) + cmd.dispatch(tilesX, tilesY, slicesZ); + // Make tilelist writes visible to fragment shader (Sync2) + vk::MemoryBarrier2 memBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eComputeShader, + .srcAccessMask = vk::AccessFlagBits2::eShaderWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead}; + vk::DependencyInfo depInfoComputeToFrag{.memoryBarrierCount = 1, .pMemoryBarriers = &memBarrier2}; + cmd.pipelineBarrier2(depInfoComputeToFrag); +} + +// Ensure compute descriptor binding 0 (lights SSBO) is bound for the given frame. +void Renderer::refreshForwardPlusComputeLightsBindingForFrame(uint32_t frameIndex) +{ + try + { + if (frameIndex >= forwardPlusPerFrame.size()) + return; + if (forwardPlusPerFrame[frameIndex].computeSet == nullptr) + return; + if (frameIndex >= lightStorageBuffers.size()) + return; + if (lightStorageBuffers[frameIndex].buffer == nullptr) + return; + + // Updating descriptor sets during recording causes validation errors: + // "commandBuffer must be in the recording state" and invalidates the command buffer. + // These descriptor sets are already initialized earlier at the safe point (line 1059), + // so this redundant update during recording is unnecessary and harmful. + if (isRecordingCmd.load(std::memory_order_relaxed)) + { + return; // Skip update, descriptor is already valid from earlier initialization + } + + vk::DescriptorBufferInfo lightsInfo{.buffer = *lightStorageBuffers[frameIndex].buffer, .offset = 0, .range = VK_WHOLE_SIZE}; + vk::WriteDescriptorSet write{.dstSet = *forwardPlusPerFrame[frameIndex].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 refresh Forward+ compute lights binding for frame " << frameIndex << ": " << e.what() << std::endl; + } } // Create compute command pool -bool Renderer::createComputeCommandPool() { - try { - vk::CommandPoolCreateInfo poolInfo{ - .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, - .queueFamilyIndex = queueFamilyIndices.computeFamily.value() - }; - - computeCommandPool = vk::raii::CommandPool(device, poolInfo); - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create compute command pool: " << e.what() << std::endl; - return false; - } +bool Renderer::createComputeCommandPool() +{ + try + { + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueFamilyIndices.computeFamily.value()}; + + computeCommandPool = vk::raii::CommandPool(device, poolInfo); + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create compute command pool: " << e.what() << std::endl; + return false; + } } // Dispatch compute shader vk::raii::Fence Renderer::DispatchCompute(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ, - vk::Buffer inputBuffer, vk::Buffer outputBuffer, - vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer) { - try { - // Create fence for synchronization - vk::FenceCreateInfo fenceInfo{}; - vk::raii::Fence computeFence(device, fenceInfo); - - // Create descriptor sets - vk::DescriptorSetAllocateInfo allocInfo{ - .descriptorPool = *computeDescriptorPool, - .descriptorSetCount = 1, - .pSetLayouts = &*computeDescriptorSetLayout - }; - - computeDescriptorSets = device.allocateDescriptorSets(allocInfo); - - // Update descriptor sets - vk::DescriptorBufferInfo inputBufferInfo{ - .buffer = inputBuffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - vk::DescriptorBufferInfo outputBufferInfo{ - .buffer = outputBuffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - vk::DescriptorBufferInfo hrtfBufferInfo{ - .buffer = hrtfBuffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - vk::DescriptorBufferInfo paramsBufferInfo{ - .buffer = paramsBuffer, - .offset = 0, - .range = VK_WHOLE_SIZE - }; - - std::array descriptorWrites = { - vk::WriteDescriptorSet{ - .dstSet = computeDescriptorSets[0], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .pBufferInfo = &inputBufferInfo - }, - vk::WriteDescriptorSet{ - .dstSet = computeDescriptorSets[0], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .pBufferInfo = &outputBufferInfo - }, - vk::WriteDescriptorSet{ - .dstSet = computeDescriptorSets[0], - .dstBinding = 2, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eStorageBuffer, - .pBufferInfo = &hrtfBufferInfo - }, - vk::WriteDescriptorSet{ - .dstSet = computeDescriptorSets[0], - .dstBinding = 3, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pBufferInfo = ¶msBufferInfo - } - }; - - device.updateDescriptorSets(descriptorWrites, {}); - - // Create command buffer using dedicated compute command pool - vk::CommandBufferAllocateInfo cmdAllocInfo{ - .commandPool = *computeCommandPool, - .level = vk::CommandBufferLevel::ePrimary, - .commandBufferCount = 1 - }; - - auto commandBuffers = device.allocateCommandBuffers(cmdAllocInfo); - // Use RAII wrapper temporarily for recording to preserve dispatch loader - vk::raii::CommandBuffer commandBufferRaii = std::move(commandBuffers[0]); - - // Begin command buffer - vk::CommandBufferBeginInfo beginInfo{ - .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit - }; - - commandBufferRaii.begin(beginInfo); - - // Bind compute pipeline - commandBufferRaii.bindPipeline(vk::PipelineBindPoint::eCompute, *computePipeline); - - // Bind descriptor sets - properly convert RAII descriptor set to regular descriptor set - std::vector descriptorSetsToBindRaw; - descriptorSetsToBindRaw.reserve(1); - descriptorSetsToBindRaw.push_back(*computeDescriptorSets[0]); - commandBufferRaii.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, descriptorSetsToBindRaw, {}); - - // Dispatch compute shader - commandBufferRaii.dispatch(groupCountX, groupCountY, groupCountZ); - - // End command buffer - commandBufferRaii.end(); - - // Extract raw command buffer for submission and release RAII ownership - // This prevents premature destruction while preserving the recorded commands - vk::CommandBuffer rawCommandBuffer = *commandBufferRaii; - commandBufferRaii.release(); // Release RAII ownership to prevent destruction - - // Submit command buffer with fence for synchronization - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &rawCommandBuffer - }; - - // Use mutex to ensure thread-safe access to compute queue - { - std::lock_guard lock(queueMutex); - computeQueue.submit(submitInfo, *computeFence); - } - - // Return fence for non-blocking synchronization - return computeFence; - } catch (const std::exception& e) { - std::cerr << "Failed to dispatch compute shader: " << e.what() << std::endl; - // Return a null fence on error - vk::FenceCreateInfo fenceInfo{}; - return {device, fenceInfo}; - } + vk::Buffer inputBuffer, vk::Buffer outputBuffer, + vk::Buffer hrtfBuffer, vk::Buffer paramsBuffer) +{ + try + { + // Create fence for synchronization + vk::FenceCreateInfo fenceInfo{}; + vk::raii::Fence computeFence(device, fenceInfo); + + // Create descriptor sets + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *computeDescriptorPool, + .descriptorSetCount = 1, + .pSetLayouts = &*computeDescriptorSetLayout}; + + { + std::lock_guard lk(descriptorMutex); + computeDescriptorSets = device.allocateDescriptorSets(allocInfo); + } + + // Update descriptor sets + vk::DescriptorBufferInfo inputBufferInfo{ + .buffer = inputBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE}; + + vk::DescriptorBufferInfo outputBufferInfo{ + .buffer = outputBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE}; + + vk::DescriptorBufferInfo hrtfBufferInfo{ + .buffer = hrtfBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE}; + + vk::DescriptorBufferInfo paramsBufferInfo{ + .buffer = paramsBuffer, + .offset = 0, + .range = VK_WHOLE_SIZE}; + + std::array descriptorWrites = { + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &inputBufferInfo}, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &outputBufferInfo}, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 2, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &hrtfBufferInfo}, + vk::WriteDescriptorSet{ + .dstSet = computeDescriptorSets[0], + .dstBinding = 3, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = ¶msBufferInfo}}; + + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + + // Create command buffer using dedicated compute command pool + vk::CommandBufferAllocateInfo cmdAllocInfo{ + .commandPool = *computeCommandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1}; + + auto commandBuffers = device.allocateCommandBuffers(cmdAllocInfo); + // Use RAII wrapper temporarily for recording to preserve dispatch loader + vk::raii::CommandBuffer commandBufferRaii = std::move(commandBuffers[0]); + + // Begin command buffer + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}; + + commandBufferRaii.begin(beginInfo); + + // Bind compute pipeline + commandBufferRaii.bindPipeline(vk::PipelineBindPoint::eCompute, *computePipeline); + + // Bind descriptor sets - properly convert RAII descriptor set to regular descriptor set + std::vector descriptorSetsToBindRaw; + descriptorSetsToBindRaw.reserve(1); + descriptorSetsToBindRaw.push_back(*computeDescriptorSets[0]); + commandBufferRaii.bindDescriptorSets(vk::PipelineBindPoint::eCompute, *computePipelineLayout, 0, descriptorSetsToBindRaw, {}); + + // Dispatch compute shader + commandBufferRaii.dispatch(groupCountX, groupCountY, groupCountZ); + + // End command buffer + commandBufferRaii.end(); + + // Extract raw command buffer for submission and release RAII ownership + // This prevents premature destruction while preserving the recorded commands + vk::CommandBuffer rawCommandBuffer = *commandBufferRaii; + commandBufferRaii.release(); // Release RAII ownership to prevent destruction + + // Submit command buffer with fence for synchronization + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &rawCommandBuffer}; + + // Use mutex to ensure thread-safe access to compute queue + { + std::lock_guard lock(queueMutex); + computeQueue.submit(submitInfo, *computeFence); + } + + // Return fence for non-blocking synchronization + return computeFence; + } + catch (const std::exception &e) + { + std::cerr << "Failed to dispatch compute shader: " << e.what() << std::endl; + // Return a null fence on error + vk::FenceCreateInfo fenceInfo{}; + return {device, fenceInfo}; + } } diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index b6252161..6141a161 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -1,633 +1,1177 @@ +/* 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 #include -VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file -#include #include +#include // Debug callback for vk::raii static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( - vk::DebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - vk::DebugUtilsMessageTypeFlagsEXT messageType, - const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, - void* pUserData) { - - if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { - // 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; + vk::DebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + vk::DebugUtilsMessageTypeFlagsEXT messageType, + const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData, + void *pUserData) +{ + if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) + { + // 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; +} + +// Watchdog thread function - monitors frame updates and aborts if application hangs +static void WatchdogThreadFunc(std::atomic *lastFrameTime, + std::atomic *running) +{ + std::cout << "[Watchdog] Started - will abort if no frame updates for 5+ seconds\n"; + + 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 in the last 5 seconds + // 5 seconds allows for heavy GPU workloads (ray query with 552 meshes + reflections/transparency) + auto now = std::chrono::steady_clock::now(); + auto lastUpdate = lastFrameTime->load(std::memory_order_relaxed); + auto elapsed = std::chrono::duration_cast(now - lastUpdate).count(); + + if (elapsed >= 5) + { + // APPLICATION HAS HUNG - no frame updates for 5+ seconds + 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"; + 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; +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(); +Renderer::~Renderer() +{ + Cleanup(); } // Initialize the renderer -bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { - vk::detail::DynamicLoader dl; - auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); - VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); - // Create a Vulkan instance - if (!createInstance(appName, enableValidationLayers)) { - return false; - } - - // Setup debug messenger - if (!setupDebugMessenger(enableValidationLayers)) { - return false; - } - - // Create surface - if (!createSurface()) { - return false; - } - - // Pick the physical device - if (!pickPhysicalDevice()) { - return false; - } - - // Create logical device - if (!createLogicalDevice(enableValidationLayers)) { - 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 - if (!memoryPool->preAllocatePools()) { - std::cerr << "Failed to pre-allocate memory pools" << std::endl; - return false; - } - } catch (const std::exception& e) { - std::cerr << "Failed to create memory pool: " << e.what() << std::endl; - return false; - } - - // Create swap chain - if (!createSwapChain()) { - return false; - } - - // Create image views - if (!createImageViews()) { - return false; - } - - // Setup dynamic rendering - if (!setupDynamicRendering()) { - return false; - } - - // Create the descriptor set layout - if (!createDescriptorSetLayout()) { - return false; - } - - // Create the graphics pipeline - if (!createGraphicsPipeline()) { - return false; - } - - // Create PBR pipeline - if (!createPBRPipeline()) { - return false; - } - - // Create the lighting pipeline - if (!createLightingPipeline()) { - std::cerr << "Failed to create lighting pipeline" << std::endl; - return false; - } - - // Create compute pipeline - if (!createComputePipeline()) { - std::cerr << "Failed to create compute pipeline" << std::endl; - return false; - } - - // Create the command pool - if (!createCommandPool()) { - return false; - } - - // Create depth resources - if (!createDepthResources()) { - return false; - } - - // Create the descriptor pool - if (!createDescriptorPool()) { - return false; - } - - if (!createOrResizeLightStorageBuffers(1)) { - std::cerr << "Failed to create initial light storage buffers" << std::endl; - return false; - } - - if (!createOpaqueSceneColorResources()) { - 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()) { - return false; - } - - // Create sync objects - if (!createSyncObjects()) { - 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; - } - - initialized = true; - return true; +bool Renderer::Initialize(const std::string &appName, bool enableValidationLayers) +{ + vk::detail::DynamicLoader dl; + auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); + // Create a Vulkan instance + if (!createInstance(appName, enableValidationLayers)) + { + return false; + } + + // Setup debug messenger + if (!setupDebugMessenger(enableValidationLayers)) + { + return false; + } + + // Create surface + if (!createSurface()) + { + return false; + } + + // Pick the physical device + if (!pickPhysicalDevice()) + { + return false; + } + + // Create logical device + if (!createLogicalDevice(enableValidationLayers)) + { + 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()) + { + return false; + } + + // Create image views + if (!createImageViews()) + { + return false; + } + + // Setup dynamic rendering + if (!setupDynamicRendering()) + { + return false; + } + + // Create the descriptor set layout + if (!createDescriptorSetLayout()) + { + return false; + } + + // Create the graphics pipeline + if (!createGraphicsPipeline()) + { + return false; + } + + // Create PBR pipeline + if (!createPBRPipeline()) + { + 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()) + { + return false; + } + + // Create depth resources + if (!createDepthResources()) + { + return false; + } + + if (useForwardPlus) + { + if (!createDepthPrepassPipeline()) + { + return false; + } + } + + // Create the descriptor pool + if (!createDescriptorPool()) + { + 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()) + { + 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()) + { + return false; + } + + // Create sync objects + if (!createSyncObjects()) + { + 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); + + 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 { - vk::detail::DynamicLoader dl; - auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); - if (vkGetInstanceProcAddr) { - 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 - } +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 + { + vk::detail::DynamicLoader dl; + auto vkGetInstanceProcAddr = dl.getProcAddress("vkGetInstanceProcAddr"); + if (vkGetInstanceProcAddr) + { + 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() { - // Ensure background workers are stopped before tearing down Vulkan resources - { - std::unique_lock lock(threadPoolMutex); - if (threadPool) { - threadPool.reset(); - } - } - if (initialized) { - std::cout << "Starting renderer cleanup..." << std::endl; - - // Wait for the device to be idle before cleaning up - device.waitIdle(); - for (auto& resources : entityResources | std::views::values) { - // Memory pool handles unmapping automatically, no need to manually unmap - resources.basicDescriptorSets.clear(); - resources.pbrDescriptorSets.clear(); - resources.uniformBuffers.clear(); - resources.uniformBufferAllocations.clear(); - resources.uniformBuffersMapped.clear(); - } - // Also clear global descriptor sets that are allocated from descriptorPool, so they are - // destroyed while the pool is still valid (avoid vkFreeDescriptorSets invalid pool errors) - transparentDescriptorSets.clear(); - transparentFallbackDescriptorSets.clear(); - computeDescriptorSets.clear(); - std::cout << "Renderer cleanup completed." << std::endl; - initialized = false; - } +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; + opaqueSceneColorImageView = nullptr; + opaqueSceneColorImageMemory = nullptr; + opaqueSceneColorImage = nullptr; + + // 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 +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); + 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); - } - - // 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(); - - // Enable debug printf functionality for shader debugging - enabledValidationFeatures.push_back(vk::ValidationFeatureEnableEXT::eDebugPrintf); - - validationFeatures.enabledValidationFeatureCount = static_cast(enabledValidationFeatures.size()); - validationFeatures.pEnabledValidationFeatures = enabledValidationFeatures.data(); - - createInfo.pNext = &validationFeatures; - } - - // Create instance - instance = vk::raii::Instance(context, createInfo); - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create instance: " << e.what() << std::endl; - return false; - } + // Add debug extension if validation layers are enabled + if (enableValidationLayers) + { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + // 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); + 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{ - .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | - vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | - vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | - vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, - .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | - vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | - vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance, - .pfnUserCallback = debugCallbackVkRaii - }; - - // 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; - } +bool Renderer::setupDebugMessenger(bool enableValidationLayers) +{ + if (!enableValidationLayers) + { + return true; + } + + try + { + // Create debug messenger info + vk::DebugUtilsMessengerCreateInfoEXT createInfo{ + .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, + .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance, + .pfnUserCallback = debugCallbackVkRaii}; + + // 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; - } +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; - - // 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; - } +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; + + // 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); } - - // First, handle dependency: VK_EXT_attachment_feedback_loop_dynamic_state requires VK_EXT_attachment_feedback_loop_layout - const char* dynState = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME; - const char* layoutReq = "VK_EXT_attachment_feedback_loop_layout"; - bool dynSupported = avail.contains(dynState); - bool layoutSupported = avail.contains(layoutReq); - for (const auto& optionalExt : optionalDeviceExtensions) { - if (std::strcmp(optionalExt, dynState) == 0) { - if (dynSupported && layoutSupported) { - deviceExtensions.push_back(dynState); - deviceExtensions.push_back(layoutReq); - std::cout << "Adding optional extension: " << dynState << std::endl; - std::cout << "Adding required-by-optional extension: " << layoutReq << std::endl; - } else if (dynSupported && !layoutSupported) { - std::cout << "Skipping extension due to missing dependency: " << dynState << " requires " << layoutReq << std::endl; - } - continue; // handled - } - if (avail.contains(optionalExt)) { - deviceExtensions.push_back(optionalExt); - std::cout << "Adding optional extension: " << optionalExt << std::endl; - } - } - } catch (const std::exception& e) { - std::cerr << "Warning: Failed to add optional extensions: " << e.what() << std::endl; - } +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); + } + + // First, handle dependency: VK_EXT_attachment_feedback_loop_dynamic_state requires VK_EXT_attachment_feedback_loop_layout + const char *dynState = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME; + const char *layoutReq = "VK_EXT_attachment_feedback_loop_layout"; + bool dynSupported = avail.contains(dynState); + bool layoutSupported = avail.contains(layoutReq); + for (const auto &optionalExt : optionalDeviceExtensions) + { + if (std::strcmp(optionalExt, dynState) == 0) + { + if (dynSupported && layoutSupported) + { + deviceExtensions.push_back(dynState); + deviceExtensions.push_back(layoutReq); + std::cout << "Adding optional extension: " << dynState << std::endl; + std::cout << "Adding required-by-optional extension: " << layoutReq << std::endl; + } + else if (dynSupported && !layoutSupported) + { + std::cout << "Skipping extension due to missing dependency: " << dynState << " requires " << layoutReq << std::endl; + } + continue; // handled + } + if (avail.contains(optionalExt)) + { + deviceExtensions.push_back(optionalExt); + std::cout << "Adding optional extension: " << optionalExt << std::endl; + } + } + } + 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 = { +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); - } - - // Enable required features - auto features = physicalDevice.getFeatures2(); - features.features.samplerAnisotropy = vk::True; - features.features.depthBiasClamp = vk::True; - - // 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; - - // Vulkan memory model features (required for some shader operations) - vk::PhysicalDeviceVulkanMemoryModelFeatures memoryModelFeatures; - memoryModelFeatures.vulkanMemoryModel = vk::True; - memoryModelFeatures.vulkanMemoryModelDeviceScope = vk::True; - - // 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 = vk::True; - - // Enable Vulkan 1.3 features - vk::PhysicalDeviceVulkan13Features vulkan13Features; - vulkan13Features.dynamicRendering = vk::True; - vulkan13Features.synchronization2 = vk::True; - - // Chain the feature structures together - timelineSemaphoreFeatures.pNext = &memoryModelFeatures; - memoryModelFeatures.pNext = &bufferDeviceAddressFeatures; - bufferDeviceAddressFeatures.pNext = &storage8BitFeatures; - storage8BitFeatures.pNext = &vulkan13Features; - features.pNext = &timelineSemaphoreFeatures; - - // 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); - - // 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::SemaphoreTypeCreateInfo typeInfo{ - .semaphoreType = vk::SemaphoreType::eTimeline, - .initialValue = 0 - }; - vk::SemaphoreCreateInfo timelineCreateInfo{ .pNext = &typeInfo }; - uploadsTimeline = vk::raii::Semaphore(device, timelineCreateInfo); - 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; - } + 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); + } + + // Enable required features + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = vk::True; + features.features.depthBiasClamp = vk::True; + + // 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; + + // Vulkan memory model features (required for some shader operations) + vk::PhysicalDeviceVulkanMemoryModelFeatures memoryModelFeatures; + memoryModelFeatures.vulkanMemoryModel = vk::True; + memoryModelFeatures.vulkanMemoryModelDeviceScope = vk::True; + + // 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 = vk::True; + + // 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 + auto featureChain = physicalDevice.getFeatures2< + vk::PhysicalDeviceFeatures2, + vk::PhysicalDeviceDescriptorIndexingFeatures, + vk::PhysicalDeviceRobustness2FeaturesEXT, + vk::PhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, + vk::PhysicalDeviceShaderTileImageFeaturesEXT, + vk::PhysicalDeviceAccelerationStructureFeaturesKHR, + vk::PhysicalDeviceRayQueryFeaturesKHR>(); + const auto &coreFeaturesSupported = featureChain.get().features; + const auto &indexingFeaturesSupported = featureChain.get(); + const auto &robust2Supported = featureChain.get(); + const auto &localReadSupported = featureChain.get(); + const auto &tileImageSupported = 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; + + // Prepare Robustness2 features if the extension is enabled and device supports + auto hasRobust2 = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) != deviceExtensions.end(); + 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; + } + + // Prepare Dynamic Rendering Local Read features if extension is enabled and supported + auto hasLocalRead = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME) != deviceExtensions.end(); + vk::PhysicalDeviceDynamicRenderingLocalReadFeaturesKHR localReadEnable{}; + if (hasLocalRead && localReadSupported.dynamicRenderingLocalRead) + { + localReadEnable.dynamicRenderingLocalRead = vk::True; + } + + // Prepare Shader Tile Image features if extension is enabled and supported + auto hasTileImage = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME) != deviceExtensions.end(); + 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; + } + + // Prepare Acceleration Structure features if extension is enabled and supported + auto hasAccelerationStructure = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) != deviceExtensions.end(); + vk::PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureEnable{}; + if (hasAccelerationStructure && accelerationStructureSupported.accelerationStructure) + { + accelerationStructureEnable.accelerationStructure = vk::True; + } + + // Prepare Ray Query features if extension is enabled and supported + auto hasRayQuery = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_RAY_QUERY_EXTENSION_NAME) != deviceExtensions.end(); + vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryEnable{}; + if (hasRayQuery && rayQuerySupported.rayQuery) + { + rayQueryEnable.rayQuery = vk::True; + } + + // Chain the feature structures together (build pNext chain explicitly) + // Base + features.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 + vulkan13Features.pNext = nullptr; + void **tailNext = reinterpret_cast(&vulkan13Features.pNext); + if (descriptorIndexingEnabled) + { + indexingFeaturesEnable.pNext = nullptr; + *tailNext = &indexingFeaturesEnable; + tailNext = reinterpret_cast(&indexingFeaturesEnable.pNext); + } + if (hasRobust2) + { + robust2Enable.pNext = nullptr; + *tailNext = &robust2Enable; + tailNext = reinterpret_cast(&robust2Enable.pNext); + } + if (hasLocalRead) + { + localReadEnable.pNext = nullptr; + *tailNext = &localReadEnable; + tailNext = reinterpret_cast(&localReadEnable.pNext); + } + if (hasTileImage) + { + tileImageEnable.pNext = nullptr; + *tailNext = &tileImageEnable; + tailNext = reinterpret_cast(&tileImageEnable.pNext); + } + if (hasAccelerationStructure) + { + accelerationStructureEnable.pNext = nullptr; + *tailNext = &accelerationStructureEnable; + tailNext = reinterpret_cast(&accelerationStructureEnable.pNext); + } + if (hasRayQuery) + { + rayQueryEnable.pNext = nullptr; + *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); + dynamicRenderingLocalReadEnabled = hasLocalRead && (localReadEnable.dynamicRenderingLocalRead == vk::True); + shaderTileImageEnabled = hasTileImage && (tileImageEnable.shaderTileImageColorReadAccess == vk::True || + tileImageEnable.shaderTileImageDepthReadAccess == vk::True || + tileImageEnable.shaderTileImageStencilReadAccess == vk::True); + 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); + + // 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::SemaphoreTypeCreateInfo typeInfo{ + .semaphoreType = vk::SemaphoreType::eTimeline, + .initialValue = 0}; + vk::SemaphoreCreateInfo timelineCreateInfo{.pNext = &typeInfo}; + uploadsTimeline = vk::raii::Semaphore(device, timelineCreateInfo); + 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; +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/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index fef262b3..778b2429 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -1,756 +1,1360 @@ +/* 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 #include -#include "mesh_component.h" // 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}; - vk::DescriptorSetLayoutCreateInfo layoutInfo{ - .bindingCount = static_cast(bindings.size()), - .pBindings = bindings.data() - }; - - 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 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 - } - }; - - // Create a descriptor set layout - vk::DescriptorSetLayoutCreateInfo layoutInfo{ - .bindingCount = static_cast(bindings.size()), - .pBindings = bindings.data() - }; - - 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 }; - 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; - } +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}}; + + // 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; + for (int i = 1; i <= 5; ++i) + { + bindingFlags[i] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; + } + // NOTE: Bindings 6/7/8 are storage buffers. We cannot use UPDATE_AFTER_BIND for them because + // descriptorBindingStorageBufferUpdateAfterBind feature is not enabled. These bindings should + // only be updated when buffers change, not every frame. + // Binding 10 (reflection sampler) can be updated after bind + 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; + } + 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 = { +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 - // Removed to fix validation layer error - shaders don't expect input at location 11 - - 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 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{ - .sType = vk::StructureType::ePipelineRenderingCreateInfo, - .pNext = nullptr, - .colorAttachmentCount = 1, - .pColorAttachmentFormats = &swapChainImageFormat, - .depthAttachmentFormat = depthFormat, - .stencilAttachmentFormat = vk::Format::eUndefined - }; - - // Create the graphics pipeline - vk::PipelineRasterizationStateCreateInfo rasterizerBack = rasterizer; - rasterizerBack.cullMode = vk::CullModeFlagBits::eBack; - - vk::GraphicsPipelineCreateInfo pipelineInfo{ - .sType = vk::StructureType::eGraphicsPipelineCreateInfo, - .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; - } + 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{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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 = { +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{ - .sType = vk::StructureType::ePipelineRenderingCreateInfo, - .pNext = nullptr, - .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{ - .sType = vk::StructureType::eGraphicsPipelineCreateInfo, - .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); - - // 2) Blended PBR pipeline (alpha blending, depth writes disabled for translucency) - vk::PipelineColorBlendAttachmentState blendedAttachment = colorBlendAttachment; - blendedAttachment.blendEnable = VK_TRUE; - blendedAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha; - blendedAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; - 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{ - .sType = vk::StructureType::eGraphicsPipelineCreateInfo, - .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{ - .sType = vk::StructureType::eGraphicsPipelineCreateInfo, - .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; - } + 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{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = vk::Format::eUndefined, + .stencilAttachmentFormat = vk::Format::eUndefined}; + + vk::GraphicsPipelineCreateInfo pipeInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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{ + .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{ - .sType = vk::StructureType::ePipelineRenderingCreateInfo, - .pNext = nullptr, - .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{ - .sType = vk::StructureType::eGraphicsPipelineCreateInfo, - .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; - } +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{ + .sType = vk::StructureType::ePipelineRenderingCreateInfo, + .pNext = nullptr, + .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{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .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); +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/simple_engine/renderer_ray_query.cpp b/attachments/simple_engine/renderer_ray_query.cpp new file mode 100644 index 00000000..939dc89b --- /dev/null +++ b/attachments/simple_engine/renderer_ray_query.cpp @@ -0,0 +1,1455 @@ +/* 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 "entity.h" +#include "mesh_component.h" +#include "renderer.h" +#include "transform_component.h" +#include +#include +#include +#include +#include + +// Helper function to get buffer device address +vk::DeviceAddress getBufferDeviceAddress(const vk::raii::Device &device, vk::Buffer buffer) +{ + vk::BufferDeviceAddressInfo addressInfo{}; + addressInfo.buffer = buffer; + return device.getBufferAddress(addressInfo); +} + +/** + * @brief Build acceleration structures for ray query rendering. + * + * Builds BLAS for each unique mesh and a TLAS for the entire scene. + * + * @param entities The entities to include in the acceleration structures. + * @return True if successful, false otherwise. + */ +bool Renderer::buildAccelerationStructures(const std::vector> &entities) +{ + if (!accelerationStructureEnabled || !rayQueryEnabled) + { + std::cout << "Acceleration structures not supported on this device\n"; + return false; + } + + try + { + std::cout << "Building acceleration structures for " << entities.size() << " entities...\n"; + + // PRECHECK: Determine how many renderable entities and unique meshes are READY right now. + // If the counts would shrink compared to the last successful build (e.g., streaming not done), + // skip rebuilding to avoid producing a TLAS that only contains a small subset (like animated fans). + size_t readyRenderableCount = 0; + size_t readyUniqueMeshCount = 0; + { + size_t skippedInactive = 0; + size_t skippedNoMesh = 0; + size_t skippedNoRes = 0; + size_t skippedException = 0; + + std::map meshToBLASProbe; + for (const auto &entityPtr : entities) + { + Entity *entity = entityPtr.get(); + if (!entity || !entity->IsActive()) + { + skippedInactive++; + continue; + } + auto meshComp = entity->GetComponent(); + if (!meshComp) + { + skippedNoMesh++; + continue; + } + + try + { + auto meshIt = meshResources.find(meshComp); + if (meshIt == meshResources.end()) + { + skippedNoRes++; + continue; + } + } + catch (...) + { + skippedException++; + continue; + } + + readyRenderableCount++; + if (meshToBLASProbe.find(meshComp) == meshToBLASProbe.end()) + { + meshToBLASProbe[meshComp] = static_cast(meshToBLASProbe.size()); + } + } + readyUniqueMeshCount = meshToBLASProbe.size(); + + // Keep this precheck quiet; any meaningful summary is printed in the main AS build block below. + (void) skippedInactive; + (void) skippedNoMesh; + (void) skippedNoRes; + (void) skippedException; + } + + if (readyRenderableCount == 0 || readyUniqueMeshCount == 0) + { + std::cout << "AS build skipped: no ready meshes yet (renderables=" << readyRenderableCount + << ", uniqueMeshes=" << readyUniqueMeshCount << ")\n"; + return false; + } + + // Move old AS structures to pending deletion queue + // They will be deleted after MAX_FRAMES_IN_FLIGHT frames to ensure all GPU work finishes + // This prevents "buffer destroyed while in use" errors without needing device.waitIdle() + // which would invalidate entity descriptor sets + if (!blasStructures.empty() || *tlasStructure.handle) + { + PendingASDelete pendingDelete; + pendingDelete.blasStructures = std::move(blasStructures); + pendingDelete.tlasStructure = std::move(tlasStructure); + pendingDelete.framesSinceDestroy = 0; + pendingASDeletions.push_back(std::move(pendingDelete)); + } + + // Clear the moved-from containers (they're now empty) + blasStructures.clear(); + tlasStructure = AccelerationStructure{}; + + // Map mesh components to BLAS indices + std::map meshToBLAS; + std::vector uniqueMeshes; + + // Collect unique meshes and entities + std::vector renderableEntities; + auto containsCaseInsensitive = [](const std::string &haystack, const std::string &needle) -> bool { + std::string h = haystack; + std::string n = needle; + std::transform(h.begin(), h.end(), h.begin(), [](unsigned char c) { return std::tolower(c); }); + std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) { return std::tolower(c); }); + return h.find(n) != std::string::npos; + }; + + // Collect renderable entities for AS build without spamming logs. + size_t skippedInactive = 0; + size_t skippedNoMesh = 0; + size_t skippedNoRes = 0; + size_t skippedPendingUploads = 0; + size_t skippedNullBuffers = 0; + size_t skippedZeroIndices = 0; + size_t skippedException = 0; + + for (const auto &entityPtr : entities) + { + Entity *entity = entityPtr.get(); + if (!entity || !entity->IsActive()) + { + skippedInactive++; + continue; + } + + auto meshComp = entity->GetComponent(); + if (!meshComp) + { + skippedNoMesh++; + continue; + } + + // Safely check if mesh resources exist - catch any exceptions from dereferencing potentially stale pointers + try + { + auto meshIt = meshResources.find(meshComp); + if (meshIt == meshResources.end()) + { + skippedNoRes++; + continue; + } + + // Validate that the mesh resources have valid buffers before adding to AS build + const auto &meshRes = meshIt->second; + // Only include when uploads finished (staging sizes are zero) + if (meshRes.vertexBufferSizeBytes != 0 || meshRes.indexBufferSizeBytes != 0) + { + // Skip meshes still uploading to avoid partial TLAS builds + skippedPendingUploads++; + continue; + } + // RAII handles: check if they contain valid Vulkan handles by dereferencing + if (!*meshRes.vertexBuffer || !*meshRes.indexBuffer) + { + skippedNullBuffers++; + continue; + } + + if (meshRes.indexCount == 0) + { + skippedZeroIndices++; + continue; + } + } + catch (const std::exception &e) + { + // Avoid spamming; a rebuild on the next safe frame should succeed. + skippedException++; + continue; + } + + renderableEntities.push_back(entity); + + if (meshToBLAS.find(meshComp) == meshToBLAS.end()) + { + meshToBLAS[meshComp] = static_cast(uniqueMeshes.size()); + uniqueMeshes.push_back(meshComp); + } + } + + if (uniqueMeshes.empty()) + { + return true; + } + + // One concise build summary (no per-entity spam) + std::cout << "Building AS: uniqueMeshes=" << uniqueMeshes.size() + << ", instances=" << renderableEntities.size() + << " (skipped inactive=" << skippedInactive + << ", noMesh=" << skippedNoMesh + << ", noRes=" << skippedNoRes + << ", pendingUploads=" << skippedPendingUploads + << ", nullBuffers=" << skippedNullBuffers + << ", zeroIndices=" << skippedZeroIndices + << ", exception=" << skippedException + << ")\n"; + + // Create a dedicated command pool for AS building to avoid threading issues + // The main commandPool may be in use by the render thread + vk::CommandPoolCreateInfo poolInfo{}; + poolInfo.flags = vk::CommandPoolCreateFlagBits::eTransient; + poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); + + vk::raii::CommandPool asBuildCommandPool(device, poolInfo); + + // Create command buffer for AS building + vk::CommandBufferAllocateInfo allocInfo{}; + allocInfo.commandPool = *asBuildCommandPool; + allocInfo.level = vk::CommandBufferLevel::ePrimary; + allocInfo.commandBufferCount = 1; + + vk::raii::CommandBuffers cmdBuffers(device, allocInfo); + vk::raii::CommandBuffer &cmdBuffer = cmdBuffers[0]; + + cmdBuffer.begin(vk::CommandBufferBeginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + // (Vespa-only debugging removed; keep logs quiet.) + + // Build BLAS for each unique mesh + blasStructures.resize(uniqueMeshes.size()); + + // Keep scratch buffers alive until GPU execution completes (after fence wait) + // Destroying them early causes "VkBuffer was destroy" validation errors and crashes + std::vector scratchBuffers; + std::vector> scratchAllocations; + + for (size_t i = 0; i < uniqueMeshes.size(); ++i) + { + // Update watchdog every 50 BLAS to prevent false hang detection during long AS build + if (i > 0 && i % 50 == 0) + { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + MeshComponent *meshComp = uniqueMeshes[i]; + auto &meshRes = meshResources.at(meshComp); + + // Get buffer device addresses + vk::DeviceAddress vertexAddress = getBufferDeviceAddress(device, *meshRes.vertexBuffer); + vk::DeviceAddress indexAddress = getBufferDeviceAddress(device, *meshRes.indexBuffer); + + // Compute vertex and index counts for this mesh + const uint32_t vertexCount = static_cast(meshComp->GetVertices().size()); + const auto &indicesCPU = meshComp->GetIndices(); + uint32_t maxIndexValue = 0; + if (!indicesCPU.empty()) + { + // Find the maximum index actually referenced by this mesh + maxIndexValue = *std::max_element(indicesCPU.begin(), indicesCPU.end()); + } + + // Create geometry info + vk::AccelerationStructureGeometryKHR geometry{}; + geometry.geometryType = vk::GeometryTypeKHR::eTriangles; + // Mark geometry as OPAQUE to ensure closest hits are committed reliably for primary rays + // (we can re-introduce transparency later with any-hit/candidate handling) + geometry.flags = vk::GeometryFlagBitsKHR::eOpaque; + + geometry.geometry.triangles.vertexFormat = vk::Format::eR32G32B32Sfloat; + geometry.geometry.triangles.vertexData = vertexAddress; + geometry.geometry.triangles.vertexStride = sizeof(Vertex); + // Set maxVertex to the total vertex count for this mesh. This is the most robust + // setting across drivers and content, and avoids culling triangles that reference + // high vertex indices (observed to hide unique, single-instance meshes). + geometry.geometry.triangles.maxVertex = vertexCount; + geometry.geometry.triangles.indexType = vk::IndexType::eUint32; + geometry.geometry.triangles.indexData = indexAddress; + + // Build info + vk::AccelerationStructureBuildGeometryInfoKHR buildInfo{}; + buildInfo.type = vk::AccelerationStructureTypeKHR::eBottomLevel; + buildInfo.flags = vk::BuildAccelerationStructureFlagBitsKHR::ePreferFastTrace; + buildInfo.mode = vk::BuildAccelerationStructureModeKHR::eBuild; + buildInfo.geometryCount = 1; + buildInfo.pGeometries = &geometry; + + uint32_t primitiveCount = meshRes.indexCount / 3; + + // Get size requirements + vk::AccelerationStructureBuildSizesInfoKHR sizeInfo = device.getAccelerationStructureBuildSizesKHR( + vk::AccelerationStructureBuildTypeKHR::eDevice, + buildInfo, + primitiveCount); + + // Create BLAS buffer + auto [blasBuffer, blasAlloc] = createBufferPooled( + sizeInfo.accelerationStructureSize, + vk::BufferUsageFlagBits::eAccelerationStructureStorageKHR | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Create acceleration structure + vk::AccelerationStructureCreateInfoKHR createInfo{}; + createInfo.buffer = *blasBuffer; + createInfo.size = sizeInfo.accelerationStructureSize; + createInfo.type = vk::AccelerationStructureTypeKHR::eBottomLevel; + + vk::raii::AccelerationStructureKHR blasHandle(device, createInfo); + + // Create scratch buffer + auto [scratchBuffer, scratchAlloc] = createBufferPooled( + sizeInfo.buildScratchSize, + vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + vk::DeviceAddress scratchAddress = getBufferDeviceAddress(device, *scratchBuffer); + + // Update build info with handles (dereference RAII handle) + buildInfo.dstAccelerationStructure = *blasHandle; + buildInfo.scratchData = scratchAddress; + + // Keep scratch buffer alive until after GPU execution (after fence wait) + scratchBuffers.push_back(std::move(scratchBuffer)); + scratchAllocations.push_back(std::move(scratchAlloc)); + + // Build range info + vk::AccelerationStructureBuildRangeInfoKHR rangeInfo{}; + rangeInfo.primitiveCount = primitiveCount; + rangeInfo.primitiveOffset = 0; + rangeInfo.firstVertex = 0; + rangeInfo.transformOffset = 0; + + // Record build command - Vulkan-Hpp RAII takes array spans, not pointers + std::array rangeInfos = {&rangeInfo}; + cmdBuffer.buildAccelerationStructuresKHR(buildInfo, rangeInfos); + + // Get device address (dereference RAII handle) + vk::AccelerationStructureDeviceAddressInfoKHR addressInfo{}; + addressInfo.accelerationStructure = *blasHandle; + vk::DeviceAddress blasAddress = device.getAccelerationStructureAddressKHR(addressInfo); + + // Store BLAS (move RAII handle to avoid copy) + blasStructures[i].buffer = std::move(blasBuffer); + blasStructures[i].allocation = std::move(blasAlloc); + blasStructures[i].handle = std::move(blasHandle); + blasStructures[i].deviceAddress = blasAddress; + + // (Per-BLAS logging removed; keep logs quiet in production.) + } + + // Barrier between BLAS and TLAS builds + + // Barrier between BLAS and TLAS builds + vk::MemoryBarrier2 barrier{}; + barrier.srcStageMask = vk::PipelineStageFlagBits2::eAccelerationStructureBuildKHR; + barrier.srcAccessMask = vk::AccessFlagBits2::eAccelerationStructureWriteKHR; + barrier.dstStageMask = vk::PipelineStageFlagBits2::eAccelerationStructureBuildKHR; + barrier.dstAccessMask = vk::AccessFlagBits2::eAccelerationStructureReadKHR; + + vk::DependencyInfo depInfo{}; + depInfo.memoryBarrierCount = 1; + depInfo.pMemoryBarriers = &barrier; + cmdBuffer.pipelineBarrier2(depInfo); + + // Build TLAS with instances + std::vector instances; + instances.reserve(renderableEntities.size()); + + // Build per-instance geometry info in the SAME order as TLAS instances + std::vector geometryInfos; // defined later in file; we reuse the type + geometryInfos.reserve(renderableEntities.size()); + tlasInstanceOrder.clear(); + + // Ray Query texture table (binding 6): seed reserved shared-default slots. + // We will assign per-material texture indices into this table, and the descriptor update + // will resolve each slot to either the streamed texture or a type-appropriate fallback. + rayQueryTexKeys.clear(); + rayQueryTexFallbackSlots.clear(); + rayQueryTexIndex.clear(); + rayQueryTexCount = 0; + + auto seedReservedSlot = [&](uint32_t slot, const std::string &id) { + if (rayQueryTexKeys.size() <= slot) + { + rayQueryTexKeys.resize(slot + 1); + rayQueryTexFallbackSlots.resize(slot + 1); + } + const std::string key = ResolveTextureId(id); + rayQueryTexKeys[slot] = key; + rayQueryTexFallbackSlots[slot] = slot; + rayQueryTexIndex[key] = slot; + }; + + seedReservedSlot(RQ_SLOT_DEFAULT_BASECOLOR, SHARED_DEFAULT_ALBEDO_ID); + seedReservedSlot(RQ_SLOT_DEFAULT_NORMAL, SHARED_DEFAULT_NORMAL_ID); + seedReservedSlot(RQ_SLOT_DEFAULT_METALROUGH, SHARED_DEFAULT_METALLIC_ROUGHNESS_ID); + seedReservedSlot(RQ_SLOT_DEFAULT_OCCLUSION, SHARED_DEFAULT_OCCLUSION_ID); + seedReservedSlot(RQ_SLOT_DEFAULT_EMISSIVE, SHARED_DEFAULT_EMISSIVE_ID); + rayQueryTexCount = static_cast(rayQueryTexKeys.size()); + + auto addTextureSlot = [&](const std::string &texId, uint32_t fallbackSlot) -> uint32_t { + if (texId.empty()) + return fallbackSlot; + std::string key = ResolveTextureId(texId); + auto it = rayQueryTexIndex.find(key); + if (it != rayQueryTexIndex.end()) + return it->second; + if (rayQueryTexCount >= RQ_MAX_TEX) + return fallbackSlot; + + uint32_t slot = rayQueryTexCount; + rayQueryTexKeys.push_back(key); + rayQueryTexFallbackSlots.push_back(fallbackSlot); + rayQueryTexIndex[key] = slot; + rayQueryTexCount++; + + // Ensure streaming is requested (CPU-side decode can happen off-thread; GPU upload stays on main thread). + try + { + RegisterTextureUser(key, nullptr); + } + catch (...) + {} + return slot; + }; + + uint32_t runningInstanceIndex = 0; + for (auto entity : renderableEntities) + { + auto meshComp = entity->GetComponent(); + uint32_t blasIndex = meshToBLAS.at(meshComp); + + auto transform = entity->GetComponent(); + const glm::mat4 entityModel = transform ? transform->GetModelMatrix() : glm::mat4(1.0f); + + // Use per-instance transforms whenever at least one instance exists, even if only one. + const size_t meshInstCount = meshComp->GetInstanceCount(); + const bool hasInstance = (meshInstCount > 0); + const size_t instCount = std::max(1, meshInstCount); + + for (size_t iInst = 0; iInst < instCount; ++iInst) + { + glm::mat4 finalModel = entityModel; + if (hasInstance && iInst < meshInstCount) + { + const InstanceData &id = meshComp->GetInstance(iInst); + finalModel = entityModel * id.getModelMatrix(); // match raster path: ubo.model * instanceModel + } + + // Convert to Vulkan 3x4 row-major transform + const float *m = glm::value_ptr(finalModel); + vk::TransformMatrixKHR vkTransform; + for (int row = 0; row < 3; row++) + { + for (int col = 0; col < 4; col++) + { + vkTransform.matrix[row][col] = m[col * 4 + row]; + } + } + + // (Debug TLAS-XFORM logs removed for production) + + vk::AccelerationStructureInstanceKHR AS_Instance{}; + AS_Instance.transform = vkTransform; + AS_Instance.instanceCustomIndex = runningInstanceIndex; // per-instance sequential index + // Instance mask: include all instances by default. + AS_Instance.mask = 0xFF; + // Mirror the per-instance index into the SBT record offset so either + // CommittedInstanceID() or CommittedInstanceContributionToHitGroupIndex() + // can be used in the shader to recover the per-instance index. + AS_Instance.instanceShaderBindingTableRecordOffset = runningInstanceIndex; + // Disable facing cull at the instance level to ensure both front and back faces + // are considered during traversal. + // + // IMPORTANT: For alpha-masked materials (foliage), we must NOT force opaque. + // Ray Query inline traversal has no any-hit shader, so we emulate alpha testing + // by committing candidates only when baseColor alpha passes the cutoff. + VkGeometryInstanceFlagsKHR instFlags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR; + bool forceNoOpaque = false; + { + // Determine alpha mode for this entity's material. + // Entity name format: "modelName_Material__". + const std::string &entityName = entity->GetName(); + size_t matPos = entityName.find("_Material_"); + if (matPos != std::string::npos) + { + size_t numStart = matPos + 10; + size_t numEnd = entityName.find('_', numStart); + if (numEnd != std::string::npos && numEnd + 1 < entityName.size() && modelLoader) + { + std::string matName = entityName.substr(numEnd + 1); + if (Material *m = modelLoader->GetMaterial(matName)) + { + // Only MASK requires candidate hits for alpha test. + forceNoOpaque = (m->alphaMode == "MASK"); + } + } + } + } + instFlags |= forceNoOpaque ? VK_GEOMETRY_INSTANCE_FORCE_NO_OPAQUE_BIT_KHR : VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR; + AS_Instance.flags = static_cast(instFlags); + AS_Instance.accelerationStructureReference = blasStructures[blasIndex].deviceAddress; + instances.push_back(AS_Instance); + + // Track mapping for refit + TlasInstanceRef ref{}; + ref.entity = entity; + ref.instanced = hasInstance; + ref.instanceIndex = static_cast(hasInstance ? iInst : 0); + tlasInstanceOrder.push_back(ref); + + // Build geometry info entry for this instance (addresses identical for all instances of same mesh) + const auto &meshRes = meshResources.at(meshComp); + vk::DeviceAddress vertexAddr = getBufferDeviceAddress(device, *meshRes.vertexBuffer); + vk::DeviceAddress indexAddr = getBufferDeviceAddress(device, *meshRes.indexBuffer); + + // Extract material index from entity name (model_Material_{index}_materialName) + int32_t materialIndex = -1; + { + const std::string &entityName = entity->GetName(); + size_t matPos = entityName.find("_Material_"); + if (matPos != std::string::npos) + { + size_t numStart = matPos + 10; // length of "_Material_" + size_t numEnd = entityName.find('_', numStart); + if (numEnd != std::string::npos) + { + try + { + materialIndex = std::stoi(entityName.substr(numStart, numEnd - numStart)); + } + catch (...) + { + materialIndex = -1; + } + } + } + } + + GeometryInfo gi{}; + gi.vertexBufferAddress = vertexAddr; + gi.indexBufferAddress = indexAddr; + gi.vertexCount = static_cast(meshComp->GetVertices().size()); + gi.materialIndex = static_cast(std::max(0, materialIndex)); + // Provide indexCount so shader can bound-check primitiveIndex safely + gi.indexCount = meshRes.indexCount; + gi._pad0 = 0; + // Store normal transform for correct world-space normals and tangent-space normal mapping. + // Use the full per-instance finalModel (entityModel * instanceModel) to match raster. + { + glm::mat3 nrm = glm::transpose(glm::inverse(glm::mat3(finalModel))); + gi.normalMatrix0 = glm::vec4(nrm[0], 0.0f); + gi.normalMatrix1 = glm::vec4(nrm[1], 0.0f); + gi.normalMatrix2 = glm::vec4(nrm[2], 0.0f); + } + geometryInfos.push_back(gi); + + runningInstanceIndex++; + } + } + + // Build TLAS + + // Create instances buffer (persistent for TLAS UPDATE/Refit) + vk::DeviceSize instancesSize = sizeof(vk::AccelerationStructureInstanceKHR) * instances.size(); + auto [instancesBufferTmp, instancesAllocTmp] = createBufferPooled( + instancesSize, + vk::BufferUsageFlagBits::eAccelerationStructureBuildInputReadOnlyKHR | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Upload instances - use mappedPtr directly + void *instancesData = instancesAllocTmp->mappedPtr; + if (!instancesData) + { + std::cerr << "Failed to get mapped pointer for instances buffer\n"; + return false; + } + memcpy(instancesData, instances.data(), instancesSize); + + // Persist instances buffer/allocation and order for UPDATE (refit) + tlasInstancesBuffer = std::move(instancesBufferTmp); + tlasInstancesAllocation = std::move(instancesAllocTmp); + tlasInstanceCount = static_cast(instances.size()); + // tlasInstanceOrder already filled above in the same order as 'instances' + + vk::DeviceAddress instancesAddress = getBufferDeviceAddress(device, *tlasInstancesBuffer); + + // TLAS geometry + vk::AccelerationStructureGeometryKHR tlasGeometry{}; + tlasGeometry.geometryType = vk::GeometryTypeKHR::eInstances; + // Do not force OPAQUE here; leave flags at 0 so ray queries may process + // transparency/glass more flexibly (any-hit not used in our path). + tlasGeometry.flags = {}; + tlasGeometry.geometry.instances.sType = vk::StructureType::eAccelerationStructureGeometryInstancesDataKHR; + tlasGeometry.geometry.instances.arrayOfPointers = VK_FALSE; + tlasGeometry.geometry.instances.data = instancesAddress; + + // TLAS build info + vk::AccelerationStructureBuildGeometryInfoKHR tlasBuildInfo{}; + tlasBuildInfo.type = vk::AccelerationStructureTypeKHR::eTopLevel; + tlasBuildInfo.flags = vk::BuildAccelerationStructureFlagBitsKHR::ePreferFastTrace | + vk::BuildAccelerationStructureFlagBitsKHR::eAllowUpdate; // enable UPDATE/Refit + tlasBuildInfo.mode = vk::BuildAccelerationStructureModeKHR::eBuild; + tlasBuildInfo.geometryCount = 1; + tlasBuildInfo.pGeometries = &tlasGeometry; + + auto instanceCount = static_cast(instances.size()); + + // Get TLAS size requirements + vk::AccelerationStructureBuildSizesInfoKHR tlasSizeInfo = device.getAccelerationStructureBuildSizesKHR( + vk::AccelerationStructureBuildTypeKHR::eDevice, + tlasBuildInfo, + instanceCount); + + // Create TLAS buffer + auto [tlasBuffer, tlasAlloc] = createBufferPooled( + tlasSizeInfo.accelerationStructureSize, + vk::BufferUsageFlagBits::eAccelerationStructureStorageKHR | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Create TLAS + vk::AccelerationStructureCreateInfoKHR tlasCreateInfo{}; + tlasCreateInfo.buffer = *tlasBuffer; + tlasCreateInfo.size = tlasSizeInfo.accelerationStructureSize; + tlasCreateInfo.type = vk::AccelerationStructureTypeKHR::eTopLevel; + + vk::raii::AccelerationStructureKHR tlasHandle(device, tlasCreateInfo); + + // Create TLAS scratch buffer (for initial build) + auto [tlasScratchBuffer, tlasScratchAlloc] = createBufferPooled( + tlasSizeInfo.buildScratchSize, + vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + vk::DeviceAddress tlasScratchAddress = getBufferDeviceAddress(device, *tlasScratchBuffer); + + // Update TLAS build info (dereference RAII handle) + tlasBuildInfo.dstAccelerationStructure = *tlasHandle; + tlasBuildInfo.scratchData = tlasScratchAddress; + + // Keep TLAS scratch buffer alive until after GPU execution (after fence wait) + scratchBuffers.push_back(std::move(tlasScratchBuffer)); + scratchAllocations.push_back(std::move(tlasScratchAlloc)); + + // Ensure/update a persistent scratch buffer for TLAS UPDATE (refit) + // Allocate once sized to updateScratchSize + if (!*tlasUpdateScratchBuffer || !tlasUpdateScratchAllocation) + { + auto [updBuf, updAlloc] = createBufferPooled( + tlasSizeInfo.updateScratchSize, + vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eDeviceLocal); + tlasUpdateScratchBuffer = std::move(updBuf); + tlasUpdateScratchAllocation = std::move(updAlloc); + } + + // TLAS build range + vk::AccelerationStructureBuildRangeInfoKHR tlasRangeInfo{}; + tlasRangeInfo.primitiveCount = instanceCount; + tlasRangeInfo.primitiveOffset = 0; + tlasRangeInfo.firstVertex = 0; + tlasRangeInfo.transformOffset = 0; + + // Build TLAS - Vulkan-Hpp RAII takes array spans, not pointers + std::array tlasRangeInfos = {&tlasRangeInfo}; + cmdBuffer.buildAccelerationStructuresKHR(tlasBuildInfo, tlasRangeInfos); + + // Get TLAS device address (dereference RAII handle) + vk::AccelerationStructureDeviceAddressInfoKHR tlasAddressInfo{}; + tlasAddressInfo.accelerationStructure = *tlasHandle; + vk::DeviceAddress tlasAddress = device.getAccelerationStructureAddressKHR(tlasAddressInfo); + + // Store TLAS (move RAII handle to avoid copy) + tlasStructure.buffer = std::move(tlasBuffer); + tlasStructure.allocation = std::move(tlasAlloc); + tlasStructure.handle = std::move(tlasHandle); + tlasStructure.deviceAddress = tlasAddress; + + cmdBuffer.end(); + + // Submit and wait + vk::SubmitInfo submitInfo{}; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &(*cmdBuffer); + + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *fence); + } + + if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + { + std::cerr << "Failed to wait for AS build fence\n"; + return false; + } + + // (Verbose TLAS composition dumps removed; keep logs quiet.) + + // Record the counts we just built so we don't rebuild with smaller subsets later + lastASBuiltBLASCount = blasStructures.size(); + lastASBuiltInstanceCount = instanceCount; + + // Build geometry info buffer PER INSTANCE (same order as TLAS instances) + // geometryInfos already populated above in TLAS instance loop + + // Create and upload geometry info buffer + if (!geometryInfos.empty()) + { + vk::DeviceSize geoInfoSize = sizeof(GeometryInfo) * geometryInfos.size(); + auto [geoInfoBuf, geoInfoAlloc] = createBufferPooled( + geoInfoSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + void *geoInfoData = geoInfoAlloc->mappedPtr; + if (geoInfoData) + { + memcpy(geoInfoData, geometryInfos.data(), geoInfoSize); + } + + geometryInfoBuffer = std::move(geoInfoBuf); + geometryInfoAllocation = std::move(geoInfoAlloc); + geometryInfoCountCPU = geometryInfos.size(); + + // (Verbose geometry info buffer stats removed.) + } + + // Build material buffer with real materials from ModelLoader + { + // Build material buffer + + // Collect unique materials with their indices from entities + std::map materialIndexToName; + + size_t entityCount = 0; + for (Entity *entity : renderableEntities) + { + std::string entityName = entity->GetName(); + + // Parse entity name: "modelName_Material_{materialIndex}_materialName" + size_t matPos = entityName.find("_Material_"); + if (matPos != std::string::npos) + { + size_t numStart = matPos + 10; // length of "_Material_" + size_t numEnd = entityName.find('_', numStart); + + if (numEnd != std::string::npos) + { + try + { + uint32_t matIndex = std::stoi(entityName.substr(numStart, numEnd - numStart)); + + // Extract material name (everything after materialIndex_) + std::string materialName = entityName.substr(numEnd + 1); + materialIndexToName[matIndex] = materialName; + } + catch (...) + { + // Failed to parse, skip + } + } + } + + entityCount++; + // Progress indicator removed (log-noise) + } + + // (Verbose material discovery logs removed.) + + // Create default material for index 0 and any missing indices + MaterialData defaultMat{}; + defaultMat.albedo = glm::vec3(0.8f, 0.8f, 0.8f); + defaultMat.metallic = 0.0f; + defaultMat.roughness = 0.5f; + defaultMat.emissive = glm::vec3(0.0f); + defaultMat.ao = 1.0f; + defaultMat.ior = 1.5f; + defaultMat.emissiveStrength = 1.0f; + defaultMat.alpha = 1.0f; + defaultMat.transmissionFactor = 0.0f; + defaultMat.alphaCutoff = 0.5f; + defaultMat.alphaMode = 0; // OPAQUE + defaultMat.isGlass = 0; + defaultMat.isLiquid = 0; + // Thick-glass defaults + defaultMat.absorptionColor = glm::vec3(1.0f); + defaultMat.absorptionDistance = 1.0f; + defaultMat.thinWalled = 1u; // default to thin to avoid over-darkening + // Texture-set flags: -1 = no texture bound for that channel + defaultMat.baseColorTextureSet = -1; + defaultMat.physicalDescriptorTextureSet = -1; + defaultMat.normalTextureSet = -1; + defaultMat.occlusionTextureSet = -1; + defaultMat.emissiveTextureSet = -1; + // Default texture indices (reserved slots) + defaultMat.baseColorTexIndex = static_cast(RQ_SLOT_DEFAULT_BASECOLOR); + defaultMat.normalTexIndex = static_cast(RQ_SLOT_DEFAULT_NORMAL); + defaultMat.physicalTexIndex = static_cast(RQ_SLOT_DEFAULT_METALROUGH); + defaultMat.occlusionTexIndex = static_cast(RQ_SLOT_DEFAULT_OCCLUSION); + defaultMat.emissiveTexIndex = static_cast(RQ_SLOT_DEFAULT_EMISSIVE); + defaultMat.useSpecGlossWorkflow = 0; + defaultMat.glossinessFactor = 1.0f; + defaultMat.specularFactor = glm::vec3(0.04f); + defaultMat.hasEmissiveStrengthExt = 0; + defaultMat._padMat[0] = defaultMat._padMat[1] = defaultMat._padMat[2] = 0; + + // Build material array with proper indexing + std::vector materials; + + // Determine max material index to size the array + uint32_t maxMaterialIndex = 0; + for (const auto &[index, name] : materialIndexToName) + { + maxMaterialIndex = std::max(maxMaterialIndex, index); + } + + // Ensure minimum size of 100 materials for safety (matches original implementation) + uint32_t materialCount = std::max(maxMaterialIndex + 1, 100u); + materials.resize(materialCount, defaultMat); + + // Capture per-material texture paths (for streaming requests and debugging) + rqMaterialTexPaths.clear(); + rqMaterialTexPaths.resize(materials.size()); + + // Populate materials from ModelLoader + uint32_t loadedCount = 0; + uint32_t glassCount = 0; + uint32_t transparentCount = 0; + if (modelLoader) + { + // Populate materials from ModelLoader + size_t matProcessed = 0; + for (const auto &[index, materialName] : materialIndexToName) + { + Material *sourceMat = modelLoader->GetMaterial(materialName); + if (sourceMat) + { + MaterialData &matData = materials[index]; + + // Copy PBR properties from Material to MaterialData + matData.albedo = sourceMat->albedo; + matData.metallic = sourceMat->metallic; + matData.emissive = sourceMat->emissive; + matData.roughness = sourceMat->roughness; + matData.ao = sourceMat->ao; + matData.ior = sourceMat->ior; + matData.emissiveStrength = sourceMat->emissiveStrength; + matData.alpha = sourceMat->alpha; + matData.transmissionFactor = sourceMat->transmissionFactor; + matData.alphaCutoff = sourceMat->alphaCutoff; + + // Thick-glass parameters (no glTF volume parsing yet; use sensible defaults) + matData.absorptionColor = glm::vec3(1.0f); + matData.absorptionDistance = 1.0f; + // Consider engine-tagged glass as thick by default; others thin + matData.thinWalled = (sourceMat->isGlass ? 0u : 1u); + // Alpha mode encoding must match `shaders/ray_query.slang`: + // 0=OPAQUE, 1=MASK, 2=BLEND + if (sourceMat->alphaMode == "MASK") + { + matData.alphaMode = 1; + } + else if (sourceMat->alphaMode == "BLEND") + { + matData.alphaMode = 2; + } + else + { + matData.alphaMode = 0; + } + // Heuristics to improve glass tagging for Ray Query path + // Many Bistro assets do not carry transmission extensions; use name hints + { + std::string lower = materialName; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + const bool hasGlassWord = lower.find("glass") != std::string::npos; + const bool isWindowPane = (lower.find("window") != std::string::npos) || (lower.find("pane") != std::string::npos); + const bool isLampGlass = (lower.find("lamp") != std::string::npos) && hasGlassWord; + const bool isGlassware = (lower.find("goblet") != std::string::npos) || (lower.find("bottle") != std::string::npos) || (lower.find("wine") != std::string::npos); + + bool markGlass = sourceMat->isGlass || hasGlassWord || isWindowPane || isLampGlass || isGlassware; + matData.isGlass = markGlass ? 1u : 0u; + matData.isLiquid = sourceMat->isLiquid ? 1u : 0u; + + // Ensure non-zero transmission for glass-like materials lacking the extension + if (markGlass && matData.transmissionFactor < 0.85f) + { + matData.transmissionFactor = 0.9f; + } + + // Thin/thick hint refinement: panes/lamps are thin shells; glassware is thick + if (isWindowPane || isLampGlass) + { + matData.thinWalled = 1u; + } + else if (isGlassware) + { + matData.thinWalled = 0u; + } + } + + // Texture-set flags (raster parity): -1 means no texture is authored for that slot. + matData.baseColorTextureSet = sourceMat->albedoTexturePath.empty() ? -1 : 0; + if (sourceMat->useSpecularGlossiness) + { + matData.physicalDescriptorTextureSet = sourceMat->specGlossTexturePath.empty() ? -1 : 0; + } + else + { + matData.physicalDescriptorTextureSet = sourceMat->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + matData.normalTextureSet = sourceMat->normalTexturePath.empty() ? -1 : 0; + matData.occlusionTextureSet = sourceMat->occlusionTexturePath.empty() ? -1 : 0; + matData.emissiveTextureSet = sourceMat->emissiveTexturePath.empty() ? -1 : 0; + + // Texture paths and stable indices into the Ray Query texture table (binding 6) + if (index < rqMaterialTexPaths.size()) + { + RQMaterialTexPaths &paths = rqMaterialTexPaths[index]; + paths.baseColor = sourceMat->albedoTexturePath; + paths.normal = sourceMat->normalTexturePath; + paths.physical = sourceMat->useSpecularGlossiness ? sourceMat->specGlossTexturePath : sourceMat->metallicRoughnessTexturePath; + paths.occlusion = sourceMat->occlusionTexturePath; + paths.emissive = sourceMat->emissiveTexturePath; + + matData.baseColorTexIndex = static_cast(addTextureSlot(paths.baseColor, RQ_SLOT_DEFAULT_BASECOLOR)); + matData.normalTexIndex = static_cast(addTextureSlot(paths.normal, RQ_SLOT_DEFAULT_NORMAL)); + matData.physicalTexIndex = static_cast(addTextureSlot(paths.physical, RQ_SLOT_DEFAULT_METALROUGH)); + matData.occlusionTexIndex = static_cast(addTextureSlot(paths.occlusion, RQ_SLOT_DEFAULT_OCCLUSION)); + matData.emissiveTexIndex = static_cast(addTextureSlot(paths.emissive, RQ_SLOT_DEFAULT_EMISSIVE)); + } + + // Specular-glossiness workflow support + matData.useSpecGlossWorkflow = sourceMat->useSpecularGlossiness ? 1 : 0; + matData.glossinessFactor = sourceMat->glossinessFactor; + matData.specularFactor = sourceMat->specularFactor; + matData.hasEmissiveStrengthExt = (std::abs(sourceMat->emissiveStrength - 1.0f) > 1e-6f) ? 1 : 0; + matData._padMat[0] = matData._padMat[1] = matData._padMat[2] = 0; + + // Track glass and transparent materials for statistics + if (sourceMat->isGlass) + { + glassCount++; + } + if (sourceMat->transmissionFactor > 0.1f) + { + transparentCount++; + } + + loadedCount++; + } + else + { + std::cerr << "Warning: Material '" << materialName + << "' not found in ModelLoader for index " << index << "\n"; + } + + matProcessed++; + } + } + else + { + std::cerr << "Warning: ModelLoader not available, using default materials\n"; + } + + // Create and upload material buffer (always create, even if no materials found) + vk::DeviceSize matSize = sizeof(MaterialData) * materials.size(); + auto [matBuf, matAlloc] = createBufferPooled( + matSize, + vk::BufferUsageFlagBits::eStorageBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + void *matData = matAlloc->mappedPtr; + if (matData) + { + memcpy(matData, materials.data(), matSize); + } + + materialBuffer = std::move(matBuf); + materialAllocation = std::move(matAlloc); + + // (Verbose material buffer stats removed.) + + // Record material count for shader-side bounds (provided via UBO) + materialCountCPU = materials.size(); + } + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to build acceleration structures: " << e.what() << std::endl; + return false; + } +} + +bool Renderer::refitTopLevelAS(const std::vector> &entities) +{ + try + { + if (!rayQueryEnabled || !accelerationStructureEnabled) + return false; + if (!*tlasStructure.handle) + return false; + if (!*tlasInstancesBuffer || !tlasInstancesAllocation || tlasInstanceOrder.size() != tlasInstanceCount) + return false; + + // Update instance transforms in the persistent instances buffer + auto *instPtr = reinterpret_cast(tlasInstancesAllocation->mappedPtr); + if (!instPtr) + return false; + + for (uint32_t i = 0; i < tlasInstanceCount; ++i) + { + const TlasInstanceRef &ref = tlasInstanceOrder[i]; + Entity *entity = ref.entity; + if (!entity || !entity->IsActive()) + continue; + auto *transform = entity->GetComponent(); + glm::mat4 entityModel = transform ? transform->GetModelMatrix() : glm::mat4(1.0f); + + // If this TLAS entry represents a MeshComponent instance, multiply by the instance's model + glm::mat4 finalModel = entityModel; + if (ref.instanced) + { + auto *meshComp = entity->GetComponent(); + if (meshComp && ref.instanceIndex < meshComp->GetInstanceCount()) + { + const InstanceData &id = meshComp->GetInstance(ref.instanceIndex); + finalModel = entityModel * id.getModelMatrix(); + } + } + + const float *m = glm::value_ptr(finalModel); + vk::TransformMatrixKHR vkTransform; + for (int row = 0; row < 3; row++) + { + for (int col = 0; col < 4; col++) + { + vkTransform.matrix[row][col] = m[col * 4 + row]; + } + } + instPtr[i].transform = vkTransform; + } + + // Prepare UPDATE build info + vk::DeviceAddress instancesAddress = getBufferDeviceAddress(device, *tlasInstancesBuffer); + + vk::AccelerationStructureGeometryKHR tlasGeometry{}; + tlasGeometry.geometryType = vk::GeometryTypeKHR::eInstances; + tlasGeometry.flags = {}; + tlasGeometry.geometry.instances.sType = vk::StructureType::eAccelerationStructureGeometryInstancesDataKHR; + tlasGeometry.geometry.instances.arrayOfPointers = VK_FALSE; + tlasGeometry.geometry.instances.data = instancesAddress; + + vk::AccelerationStructureBuildGeometryInfoKHR tlasBuildInfo{}; + tlasBuildInfo.type = vk::AccelerationStructureTypeKHR::eTopLevel; + tlasBuildInfo.flags = vk::BuildAccelerationStructureFlagBitsKHR::ePreferFastTrace | + vk::BuildAccelerationStructureFlagBitsKHR::eAllowUpdate; + tlasBuildInfo.mode = vk::BuildAccelerationStructureModeKHR::eUpdate; + tlasBuildInfo.geometryCount = 1; + tlasBuildInfo.pGeometries = &tlasGeometry; + tlasBuildInfo.srcAccelerationStructure = *tlasStructure.handle; + tlasBuildInfo.dstAccelerationStructure = *tlasStructure.handle; + + if (!*tlasUpdateScratchBuffer || !tlasUpdateScratchAllocation) + { + // No update scratch; cannot refit + return false; + } + vk::DeviceAddress updateScratch = getBufferDeviceAddress(device, *tlasUpdateScratchBuffer); + tlasBuildInfo.scratchData = updateScratch; + + vk::AccelerationStructureBuildRangeInfoKHR tlasRangeInfo{}; + tlasRangeInfo.primitiveCount = tlasInstanceCount; + tlasRangeInfo.primitiveOffset = 0; + tlasRangeInfo.firstVertex = 0; + tlasRangeInfo.transformOffset = 0; + + // Create transient command buffer for UPDATE + vk::CommandPoolCreateInfo poolInfo{}; + poolInfo.flags = vk::CommandPoolCreateFlagBits::eTransient; + poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); + vk::raii::CommandPool cmdPool(device, poolInfo); + + vk::CommandBufferAllocateInfo allocInfo{}; + allocInfo.commandPool = *cmdPool; + allocInfo.level = vk::CommandBufferLevel::ePrimary; + allocInfo.commandBufferCount = 1; + vk::raii::CommandBuffers cmdBuffers(device, allocInfo); + vk::raii::CommandBuffer &cmd = cmdBuffers[0]; + cmd.begin(vk::CommandBufferBeginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + std::array ranges = {&tlasRangeInfo}; + cmd.buildAccelerationStructuresKHR(tlasBuildInfo, ranges); + + cmd.end(); + + // Submit and wait + vk::SubmitInfo submitInfo{}; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &(*cmd); + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *fence); + } + if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + { + std::cerr << "Failed to wait for TLAS refit fence\n"; + return false; + } + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to refit TLAS: " << e.what() << std::endl; + return false; + } +} + +/** + * @brief Update ray query descriptor sets with current resources. + * + * Binds UBO, TLAS, output image, and light buffer to the descriptor set. + * + * @param frameIndex The frame index to update. + * @return True if successful, false otherwise. + */ +bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vector> &entities) +{ + if (!rayQueryEnabled || !accelerationStructureEnabled) + { + return false; + } + + // Do not update descriptors while descriptor sets are known invalid + if (!descriptorSetsValid.load(std::memory_order_relaxed)) + { + return false; + } + + // Ensure descriptor sets exist for this frame; if missing/invalid, (re)allocate them now at the safe point + auto ensureRayQuerySets = [&]() -> bool { + try + { + if (rayQueryDescriptorSets.empty() || frameIndex >= rayQueryDescriptorSets.size()) + { + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *rayQueryDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; + allocInfo.pSetLayouts = layouts.data(); + { + std::lock_guard lk(descriptorMutex); + rayQueryDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + } + } + // Validate the handle for the current frame + vk::DescriptorSet testHandle = *rayQueryDescriptorSets[frameIndex]; + if (!testHandle) + { + // Reallocate once more if handle is null + std::vector layouts(MAX_FRAMES_IN_FLIGHT, *rayQueryDescriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; + allocInfo.pSetLayouts = layouts.data(); + { + std::lock_guard lk(descriptorMutex); + rayQueryDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); + } + testHandle = *rayQueryDescriptorSets[frameIndex]; + if (!testHandle) + return false; + } + return true; + } + catch (const std::exception &e) + { + std::cerr << "Ray query descriptor set (re)allocation failed: " << e.what() << "\n"; + return false; + } + }; + if (!ensureRayQuerySets()) + { + return false; + } + + // Validate descriptor set handle is valid before dereferencing + try + { + vk::DescriptorSet testHandle = *rayQueryDescriptorSets[frameIndex]; + if (!testHandle) + { + // Try reallocate once more + if (!ensureRayQuerySets()) + return false; + } + } + catch (const std::exception &e) + { + std::cerr << "Ray query descriptor set handle invalid for frame " << frameIndex << ": " << e.what() << "\n"; + if (!ensureRayQuerySets()) + return false; + } + + // Check if TLAS handle is valid (dereference RAII handle to check underlying VkAccelerationStructureKHR) + if (!*tlasStructure.handle) + { + std::cerr << "TLAS not built - cannot update ray query descriptor sets\n"; + return false; + } + + // Frame index alignment check: ensure we are updating descriptor set for the frame being recorded + if (frameIndex != currentFrame) + { + // Not fatal, but indicates a mismatch in frame scheduling + // Avoid noisy logs every frame + } + + // TLAS is valid at this point; avoid verbose logging in default builds + vk::AccelerationStructureKHR tlasHandleValue = *tlasStructure.handle; + + if (lightStorageBuffers.empty() || frameIndex >= lightStorageBuffers.size()) + { + std::cerr << "Light storage buffers not initialized\n"; + return false; + } + + try + { + // NOTE: Ray Query no longer stores per-instance texture indices in `GeometryInfo`. + // Textures are resolved per-material via the material buffer, and the descriptor array + // is rebuilt each update from current streamed texture handles. + + std::vector writes; + + // NOTE: Do not write into mapped geometry info here. The buffer is built at AS build time + // and remains immutable to avoid races with refit and descriptor updates. + + // Binding 0: UBO - Use dedicated ray query UBO (not entity UBO) + if (rayQueryUniformBuffers.empty() || frameIndex >= rayQueryUniformBuffers.size()) + { + std::cerr << "Ray query UBO not initialized for frame " << frameIndex << "\n"; + return false; + } + + vk::DescriptorBufferInfo uboInfo{}; + uboInfo.buffer = *rayQueryUniformBuffers[frameIndex]; + uboInfo.offset = 0; + uboInfo.range = sizeof(RayQueryUniformBufferObject); + + vk::WriteDescriptorSet uboWrite{}; + uboWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + uboWrite.dstBinding = 0; + uboWrite.dstArrayElement = 0; + uboWrite.descriptorCount = 1; + uboWrite.descriptorType = vk::DescriptorType::eUniformBuffer; + uboWrite.pBufferInfo = &uboInfo; + writes.push_back(uboWrite); + + // Binding 1: TLAS (get address of underlying VkAccelerationStructureKHR) + vk::AccelerationStructureKHR tlasHandleValue = *tlasStructure.handle; + vk::WriteDescriptorSetAccelerationStructureKHR tlasInfo{}; + tlasInfo.accelerationStructureCount = 1; + tlasInfo.pAccelerationStructures = &tlasHandleValue; + + vk::WriteDescriptorSet tlasWrite{}; + tlasWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + tlasWrite.dstBinding = 1; + tlasWrite.dstArrayElement = 0; + tlasWrite.descriptorCount = 1; + tlasWrite.descriptorType = vk::DescriptorType::eAccelerationStructureKHR; + tlasWrite.pNext = &tlasInfo; + writes.push_back(tlasWrite); + + // Binding 2: Output image + vk::DescriptorImageInfo imageInfo{}; + imageInfo.imageView = *rayQueryOutputImageView; + imageInfo.imageLayout = vk::ImageLayout::eGeneral; + + vk::WriteDescriptorSet imageWrite{}; + imageWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + imageWrite.dstBinding = 2; + imageWrite.dstArrayElement = 0; + imageWrite.descriptorCount = 1; + imageWrite.descriptorType = vk::DescriptorType::eStorageImage; + imageWrite.pImageInfo = &imageInfo; + writes.push_back(imageWrite); + + // Binding 3: Light buffer + vk::DescriptorBufferInfo lightInfo{}; + lightInfo.buffer = *lightStorageBuffers[frameIndex].buffer; + lightInfo.offset = 0; + lightInfo.range = VK_WHOLE_SIZE; + + vk::WriteDescriptorSet lightWrite{}; + lightWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + lightWrite.dstBinding = 3; + lightWrite.dstArrayElement = 0; + lightWrite.descriptorCount = 1; + lightWrite.descriptorType = vk::DescriptorType::eStorageBuffer; + lightWrite.pBufferInfo = &lightInfo; + writes.push_back(lightWrite); + + // Binding 4: Geometry info buffer (vertex/index addresses + material indices) + if (*geometryInfoBuffer) + { + vk::DescriptorBufferInfo geoInfo{}; + geoInfo.buffer = *geometryInfoBuffer; + geoInfo.offset = 0; + geoInfo.range = VK_WHOLE_SIZE; + + vk::WriteDescriptorSet geoWrite{}; + geoWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + geoWrite.dstBinding = 4; + geoWrite.dstArrayElement = 0; + geoWrite.descriptorCount = 1; + geoWrite.descriptorType = vk::DescriptorType::eStorageBuffer; + geoWrite.pBufferInfo = &geoInfo; + writes.push_back(geoWrite); + } + + // Binding 5: Material buffer (PBR material properties) + if (*materialBuffer) + { + vk::DescriptorBufferInfo matInfo{}; + matInfo.buffer = *materialBuffer; + matInfo.offset = 0; + matInfo.range = VK_WHOLE_SIZE; + + vk::WriteDescriptorSet matWrite{}; + matWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + matWrite.dstBinding = 5; + matWrite.dstArrayElement = 0; + matWrite.descriptorCount = 1; + matWrite.descriptorType = vk::DescriptorType::eStorageBuffer; + matWrite.pBufferInfo = &matInfo; + writes.push_back(matWrite); + } + + // Binding 6: Ray Query texture table (combined image samplers) + // IMPORTANT: Do NOT cache VkImageView/VkSampler handles across frames; textures can stream + // and their handles may be destroyed/recreated. Instead, rebuild image infos each update. + if (rayQueryTexKeys.size() < RQ_SLOT_DEFAULT_EMISSIVE + 1 || rayQueryTexFallbackSlots.size() < RQ_SLOT_DEFAULT_EMISSIVE + 1) + { + // Should be seeded during AS build; if not, fall back to using the generic default texture in all slots. + rayQueryTexKeys.resize(RQ_SLOT_DEFAULT_EMISSIVE + 1); + rayQueryTexFallbackSlots.resize(RQ_SLOT_DEFAULT_EMISSIVE + 1); + rayQueryTexCount = std::max(rayQueryTexCount, static_cast(rayQueryTexKeys.size())); + } + + std::vector rqArray(RQ_MAX_TEX, vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}); + + const uint32_t copyCount = std::min(rayQueryTexCount, RQ_MAX_TEX); + std::shared_lock texLock(textureResourcesMutex); + + // Helper to fill a slot with a key (if ready) or fall back to its declared fallback slot. + auto fillSlot = [&](uint32_t slot) { + if (slot >= copyCount) + return; + const std::string &key = rayQueryTexKeys[slot]; + if (!key.empty()) + { + auto itTex = textureResources.find(key); + if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + { + rqArray[slot].sampler = *itTex->second.textureSampler; + rqArray[slot].imageView = *itTex->second.textureImageView; + rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + return; + } + } + + // Not ready/missing: use slot-specific fallback. + uint32_t fb = (slot < rayQueryTexFallbackSlots.size()) ? rayQueryTexFallbackSlots[slot] : RQ_SLOT_DEFAULT_BASECOLOR; + if (fb >= copyCount) + fb = RQ_SLOT_DEFAULT_BASECOLOR; + const std::string &fbKey = (fb < rayQueryTexKeys.size()) ? rayQueryTexKeys[fb] : std::string{}; + if (!fbKey.empty()) + { + auto itTex = textureResources.find(fbKey); + if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + { + rqArray[slot].sampler = *itTex->second.textureSampler; + rqArray[slot].imageView = *itTex->second.textureImageView; + rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + } + } + }; + + // Fill all active slots. + for (uint32_t i = 0; i < copyCount; ++i) + { + fillSlot(i); + } + + vk::WriteDescriptorSet texArrayWrite{}; + texArrayWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + texArrayWrite.dstBinding = 6; + texArrayWrite.dstArrayElement = 0; + texArrayWrite.descriptorCount = RQ_MAX_TEX; + texArrayWrite.descriptorType = vk::DescriptorType::eCombinedImageSampler; + texArrayWrite.pImageInfo = rqArray.data(); + writes.push_back(texArrayWrite); + + device.updateDescriptorSets(writes, nullptr); + + // No per-frame or one-shot debug prints here; keep logs quiet in production. + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to update ray query descriptor sets: " << e.what() << std::endl; + return false; + } +} diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 451ae34c..45fefbf5 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -1,973 +1,3094 @@ -#include "renderer.h" -#include "imgui_system.h" +/* 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 "model_loader.h" -#include -#include +#include "renderer.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[3] + m[2] + fp.planes[4] = m[3] + 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 + 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 (glm::dot(n, v) + p.w < 0.0f) + { + return false; // completely outside + } + } + return true; +} // This file contains rendering-related methods from the Renderer class // Create swap chain -bool Renderer::createSwapChain() { - try { - // 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); - uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; - - // Set sharing mode - if (indices.graphicsFamily != indices.presentFamily) { - createInfo.imageSharingMode = vk::SharingMode::eConcurrent; - createInfo.queueFamilyIndexCount = 2; - createInfo.pQueueFamilyIndices = queueFamilyIndices; - } 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(); - - // 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; - } +bool Renderer::createSwapChain() +{ + try + { + // 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); + uint32_t queueFamilyIndicesLoc[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + + // Set sharing mode + if (indices.graphicsFamily != indices.presentFamily) + { + createInfo.imageSharingMode = vk::SharingMode::eConcurrent; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndicesLoc; + } + 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); + } + (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } + + 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 = nullptr; + rt.colorView = nullptr; + rt.colorAlloc = nullptr; + rt.color = nullptr; + rt.depthView = nullptr; + rt.depthAlloc = nullptr; + rt.depth = nullptr; + rt.width = rt.height = 0; + } +} + +void Renderer::renderReflectionPass(vk::raii::CommandBuffer &cmd, + const glm::vec4 &planeWS, + CameraComponent *camera, + const std::vector> &entities) +{ + // Initial scaffolding: clear the reflection RT; drawing the mirrored scene will be added next. + if (reflections.empty()) + return; + auto &rt = reflections[currentFrame]; + if (rt.width == 0 || rt.height == 0 || rt.colorView == nullptr || rt.depthView == nullptr) + 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, + .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, + .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{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); + projReflected[1][1] *= -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 != nullptr) + { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrReflectionGraphicsPipeline); + } + else if (pbrGraphicsPipeline != nullptr) + { + cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrGraphicsPipeline); + } + + // Render all entities with meshes (skip transparency; glass revisit later) + for (const auto &uptr : entities) + { + Entity *entity = uptr.get(); + 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; + + // Bind geometry + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + cmd.bindVertexBuffers(0, buffers, offsets); + cmd.bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + // Populate UBO with mirrored view + clip plane and reflection flags + UniformBufferObject ubo{}; + if (auto tc = entity->GetComponent()) + ubo.model = tc->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; + updateUniformBufferInternal(currentFrame, entity, const_cast(camera), ubo); + + // Bind descriptor set (PBR) + auto &descSets = entityIt->second.pbrDescriptorSets; + if (descSets.empty() || currentFrame >= descSets.size()) + continue; + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pbrPipelineLayout, 0, {*descSets[currentFrame]}, {}); + + // Push material properties for reflection pass (use textures) + MaterialProperties mp{}; + // Neutral defaults; textures from descriptor set will provide actual albedo/normal/etc. + mp.baseColorFactor = glm::vec4(1.0f); + mp.metallicFactor = 0.0f; + mp.roughnessFactor = 0.8f; + // 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(meshIt->second.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 { - opaqueSceneColorImage.clear(); - opaqueSceneColorImageView.clear(); - opaqueSceneColorSampler.clear(); - // Resize image views vector - swapChainImageViews.clear(); - swapChainImageViews.reserve(swapChainImages.size()); - - // Create image view for each swap chain image - for (const auto& image : swapChainImages) { - // Create image view info - vk::ImageViewCreateInfo createInfo{ - .image = image, - .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 - 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; - } +bool Renderer::createImageViews() +{ + try + { + opaqueSceneColorImage.clear(); + opaqueSceneColorImageView.clear(); + opaqueSceneColorSampler.clear(); + // Resize image views vector + swapChainImageViews.clear(); + swapChainImageViews.reserve(swapChainImages.size()); + + // Create image view for each swap chain image + for (const auto &image : swapChainImages) + { + // Create image view info + vk::ImageViewCreateInfo createInfo{ + .image = image, + .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 + 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{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{ - .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; - } +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{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{ + .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 queueFamilyIndices = findQueueFamilies(physicalDevice); - - // Create command pool info - vk::CommandPoolCreateInfo poolInfo{ - .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, - .queueFamilyIndex = queueFamilyIndices.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; - } +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; - } +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(); - - // Create semaphores per swapchain image to avoid reuse issues - size_t swapchainImageCount = swapChainImages.size(); - imageAvailableSemaphores.reserve(swapchainImageCount); - renderFinishedSemaphores.reserve(swapchainImageCount); - - // Keep fences per frame in flight for CPU-GPU synchronization - inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); - - // Create semaphore and fence info - vk::SemaphoreCreateInfo semaphoreInfo{}; - vk::FenceCreateInfo fenceInfo{ - .flags = vk::FenceCreateFlagBits::eSignaled - }; - - // Create semaphores for each swapchain image - for (size_t i = 0; i < swapchainImageCount; i++) { - imageAvailableSemaphores.emplace_back(device, semaphoreInfo); - renderFinishedSemaphores.emplace_back(device, semaphoreInfo); - } - - // Create fences for each frame in flight - for (int 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; - } +bool Renderer::createSyncObjects() +{ + try + { + // Resize semaphores and fences vectors + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + + const auto semaphoreCount = static_cast(swapChainImages.size()); + imageAvailableSemaphores.reserve(semaphoreCount); + renderFinishedSemaphores.reserve(semaphoreCount); + + // Fences remain per frame-in-flight for CPU-GPU synchronization + inFlightFences.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create semaphore and fence info + vk::SemaphoreCreateInfo semaphoreInfo{}; + vk::FenceCreateInfo fenceInfo{ + .flags = vk::FenceCreateFlagBits::eSignaled}; + + // Create semaphores per swapchain image (indexed by imageIndex from acquireNextImage) + 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) + 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 = nullptr; - depthImage = 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. - - // Clean up pipelines - graphicsPipeline = nullptr; - pbrGraphicsPipeline = nullptr; - lightingPipeline = nullptr; - - // Clean up pipeline layouts - pipelineLayout = nullptr; - pbrPipelineLayout = nullptr; - lightingPipelineLayout = nullptr; - - // Clean up sync objects (they need to be recreated with new swap chain image count) - imageAvailableSemaphores.clear(); - renderFinishedSemaphores.clear(); - - // Clean up swap chain - swapChain = nullptr; +void Renderer::cleanupSwapChain() +{ + // Clean up depth resources + depthImageView = nullptr; + depthImage = 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 = nullptr; + pbrGraphicsPipeline = nullptr; + lightingPipeline = nullptr; + + // Clean up pipeline layouts + pipelineLayout = nullptr; + pbrPipelineLayout = nullptr; + lightingPipelineLayout = 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 = nullptr; } // Recreate swap chain -void Renderer::recreateSwapChain() { - // 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()) { - if (device.waitForFences(allFences, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} - } - - // Wait for the device to be idle before recreating the swap chain - device.waitIdle(); - - // Clean up old swap chain resources - cleanupSwapChain(); - - // Recreate swap chain and related resources - createSwapChain(); - createImageViews(); - setupDynamicRendering(); - createDepthResources(); - - // 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) { - if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} - } - - // Clear all entity descriptor sets since they're now invalid (allocated from the old pool) - for (auto& resources : entityResources | std::views::values) { - resources.basicDescriptorSets.clear(); - resources.pbrDescriptorSets.clear(); - } - - createGraphicsPipeline(); - createPBRPipeline(); - createLightingPipeline(); - - // Re-create command buffers to ensure fresh recording against new swapchain state - commandBuffers.clear(); - createCommandBuffers(); - currentFrame = 0; - - // Recreate descriptor sets for all entities after swapchain/pipeline rebuild - for (const auto& entity : entityResources | std::views::keys) { - 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); - } +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()) + { + if (device.waitForFences(allFences, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + { + } + } + + // 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) + { + if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + { + } + } + + // 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(); + } + } + + // 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(); + + // Destroy ray query output image resources - they're sized to old swapchain dimensions + rayQueryOutputImageView = nullptr; + rayQueryOutputImage = 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(); } // Update uniform buffer -void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera) { - // Get entity resources - auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) { - return; - } - - // Get transform component - auto transformComponent = entity->GetComponent(); - 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, camera, ubo); +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity *entity, CameraComponent *camera) +{ + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + { + return; + } + + // Get transform component + auto transformComponent = entity->GetComponent(); + 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 + + // DIAGNOSTIC: Print view matrix being set for ray query + static bool printedOnce = false; + if (!printedOnce) + { + std::cout << "[CPU VIEW MATRIX] Setting for entity '" << entity->GetName() << "':\n"; + std::cout << " [" << ubo.view[0][0] << " " << ubo.view[0][1] << " " << ubo.view[0][2] << " " << ubo.view[0][3] << "]\n"; + std::cout << " [" << ubo.view[1][0] << " " << ubo.view[1][1] << " " << ubo.view[1][2] << " " << ubo.view[1][3] << "]\n"; + std::cout << " [" << ubo.view[2][0] << " " << ubo.view[2][1] << " " << ubo.view[2][2] << " " << ubo.view[2][3] << "]\n"; + std::cout << " [" << ubo.view[3][0] << " " << ubo.view[3][1] << " " << ubo.view[3][2] << " " << ubo.view[3][3] << "]\n"; + printedOnce = true; + } + + // Continue with the rest of the uniform buffer setup + updateUniformBufferInternal(currentImage, entity, camera, ubo); } // Overloaded version that accepts a custom transform matrix -void Renderer::updateUniformBuffer(uint32_t currentImage, Entity* entity, CameraComponent* camera, const glm::mat4& customTransform) { - // 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, camera, ubo); +void Renderer::updateUniformBuffer(uint32_t currentImage, Entity *entity, CameraComponent *camera, const glm::mat4 &customTransform) +{ + // 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, camera, ubo); } // Internal helper function to complete uniform buffer setup -void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity* entity, CameraComponent* camera, UniformBufferObject& ubo) { - // Get entity resources - auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) { - return; - } - - // Use static lights loaded during model initialization. For the - // current tutorial we render a fixed "night" scene lit only by - // emissive-derived lights from the GLTF; any punctual - // directional/point/spot lights are ignored. - const std::vector& extractedLights = staticLights; - - if (!extractedLights.empty()) { - std::vector lightsSubset; - lightsSubset.reserve(std::min(extractedLights.size(), static_cast(MAX_ACTIVE_LIGHTS))); - - for (const auto& L : extractedLights) { - if (L.type != ExtractedLight::Type::Emissive) { - continue; // skip directional/point/spot lights - } - lightsSubset.push_back(L); - if (lightsSubset.size() >= MAX_ACTIVE_LIGHTS) { - break; - } - } - - if (!lightsSubset.empty()) { - // Update the light storage buffer with emissive lights only - updateLightStorageBuffer(currentImage, lightsSubset); - ubo.lightCount = static_cast(lightsSubset.size()); - } else { - ubo.lightCount = 0; - } - } else { - ubo.lightCount = 0; - } - - // Shadows removed: no shadow bias - - // Set camera position for PBR calculations - ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); - - // Set PBR parameters (use member variables for UI control) - // Clamp exposure to a sane range to avoid washout - ubo.exposure = std::clamp(this->exposure, 0.2f, 4.0f); - ubo.gamma = this->gamma; - ubo.prefilteredCubeMipLevels = 0.0f; - ubo.scaleIBLAmbient = 0.25f; - ubo.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); - - // Signal to the shader whether swapchain is sRGB (1) or not (0) using padding0 - int outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || - swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? 1 : 0; - ubo.padding0 = outputIsSRGB; - - // Copy to uniform buffer - std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity *entity, CameraComponent *camera, UniformBufferObject &ubo) +{ + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + { + return; + } + + // Use a single source of truth for the frame's light count, set in Render() + // right before the Forward+ compute dispatch. This ensures all entities see + // a consistent lightCount and that the PBR fallback loop can run when needed. + ubo.lightCount = static_cast(lastFrameLightCount); + + // Shadows removed: no shadow bias + + // Set camera position for PBR calculations + ubo.camPos = glm::vec4(camera->GetPosition(), 1.0f); + + // Set PBR parameters (use member variables for UI control) + // Clamp exposure to a sane range to avoid washout + ubo.exposure = std::clamp(this->exposure, 0.2f, 4.0f); + ubo.gamma = this->gamma; + ubo.prefilteredCubeMipLevels = 0.0f; + ubo.scaleIBLAmbient = 0.25f; + ubo.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); + // Forward+ clustered parameters for fragment shader + ubo.nearZ = camera ? camera->GetNearPlane() : 0.1f; + ubo.farZ = camera ? camera->GetFarPlane() : 1000.0f; + ubo.slicesZ = static_cast(forwardPlusSlicesZ); + + // Signal to the shader whether swapchain is sRGB (1) or not (0) using padding0 + int outputIsSRGB = (swapChainImageFormat == vk::Format::eR8G8B8A8Srgb || + swapChainImageFormat == vk::Format::eB8G8R8A8Srgb) ? + 1 : + 0; + ubo.padding0 = outputIsSRGB; + // Padding fields no longer used for runtime debug toggles + ubo.padding1 = 0.0f; + ubo.padding2 = 0.0f; + + // Planar reflections: set sampling flags/matrices for main pass; preserve reflectionPass if already set by caller + if (ubo.reflectionPass != 1) + { + // Main pass: enable planar reflection sampling for glass only when the feature is toggled + // and we have a valid previous-frame reflection render target to sample from. + ubo.reflectionPass = 0; + bool reflReady = false; + if (enablePlanarReflections && !reflections.empty()) + { + // CRITICAL FIX: Use currentFrame (frame-in-flight index) instead of currentImage (swapchain index) + // Reflection resources are per-frame-in-flight, not per-swapchain-image + uint32_t prev = currentImage > 0 ? (currentImage - 1) : (static_cast(reflections.size()) - 1); + auto &rtPrev = reflections[prev]; + reflReady = !(rtPrev.colorView == nullptr) && !(rtPrev.colorSampler == nullptr); + } + ubo.reflectionEnabled = reflReady ? 1 : 0; + ubo.reflectionVP = sampleReflectionVP; + ubo.clipPlaneWS = currentReflectionPlane; + } + + // Reflection intensity from UI + ubo.reflectionIntensity = std::clamp(reflectionIntensity, 0.0f, 2.0f); + + // Ray query rendering options from UI + ubo.enableRayQueryReflections = enableRayQueryReflections ? 1 : 0; + ubo.enableRayQueryTransparency = enableRayQueryTransparency ? 1 : 0; + + // Copy to uniform buffer (guard against null mapped pointer) + // CRITICAL FIX: Use currentImage (the frame parameter) for uniform buffer indexing + // uniformBuffersMapped is sized per-frame-in-flight, and currentImage is the frameIndex parameter passed in + void *dst = entityIt->second.uniformBuffersMapped[currentImage]; + if (!dst) + { + // Mapped pointer not available (shouldn’t happen for HostVisible/Coherent). Avoid crash and continue. + std::cerr << "Warning: UBO mapped ptr null for entity '" << entity->GetName() << "' frame " << currentImage << std::endl; + return; + } + std::memcpy(dst, &ubo, sizeof(ubo)); } // Render the scene -void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { - if (memoryPool) memoryPool->setRenderingActive(true); - struct RenderingStateGuard { MemoryPool* pool; explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} ~RenderingStateGuard() { if (pool) pool->setRenderingActive(false); } } guard(memoryPool.get()); - - if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} - - uint32_t imageIndex; - vk::ResultValue result{{},0}; - try { - result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); - } catch (const vk::OutOfDateKHRError&) { - // 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(); - recreateSwapChain(); - return; - } - - imageIndex = result.value; - - if (result.result == vk::Result::eErrorOutOfDateKHR || result.result == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { - framebufferResized.store(false, std::memory_order_relaxed); - if (imguiSystem) ImGui::EndFrame(); - recreateSwapChain(); - return; - } - if (result.result != vk::Result::eSuccess) { - throw std::runtime_error("Failed to acquire swap chain image"); - } - - device.resetFences(*inFlightFences[currentFrame]); - if (framebufferResized.load(std::memory_order_relaxed)) { recreateSwapChain(); return; } - - commandBuffers[currentFrame].reset(); - commandBuffers[currentFrame].begin(vk::CommandBufferBeginInfo()); - if (framebufferResized.load(std::memory_order_relaxed)) { commandBuffers[currentFrame].end(); recreateSwapChain(); return; } - - // Process texture streaming uploads (see Renderer::ProcessPendingTextureJobs) - - vk::raii::Pipeline* currentPipeline = nullptr; - vk::raii::PipelineLayout* currentLayout = nullptr; - std::vector blendedQueue; - std::unordered_set blendedSet; - - // 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++; - // Only perform a small amount of streaming work every few frames. - if ((streamingFrameCounter % 3) == 0) { - ProcessPendingTextureJobs(/*maxJobs=*/1, /*includeCritical=*/false, /*includeNonCritical=*/true); - } - } - - bool blockScene = false; - if (imguiSystem) { - blockScene = IsLoading(); - } - - if (!blockScene) { - for (const auto& uptr : entities) { - Entity* entity = uptr.get(); - if (!entity || !entity->IsActive()) continue; - auto meshComponent = entity->GetComponent(); - if (!meshComponent) continue; - bool useBlended = false; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { - std::string materialName = remainder.substr(nextUnderscore + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material && (material->alphaMode == "BLEND" || material->transmissionFactor > 0.001f)) { - useBlended = true; - } - } - } - } - } - if (useBlended) { - blendedQueue.push_back(entity); - blendedSet.insert(entity); - } - } - } - - // Sort transparent entities back-to-front for correct blending of nested glass/liquids - if (!blendedQueue.empty()) { - // Sort by squared distance from the camera in world space. - // Farther objects must be rendered first so that nearer glass correctly - // appears in front (standard back-to-front transparency ordering). - glm::vec3 camPos = camera ? camera->GetPosition() : glm::vec3(0.0f); - std::ranges::sort(blendedQueue, [this, camPos](Entity* a, Entity* b) { - auto* ta = (a ? a->GetComponent() : nullptr); - auto* tb = (b ? b->GetComponent() : nullptr); - glm::vec3 pa = ta ? ta->GetPosition() : glm::vec3(0.0f); - glm::vec3 pb = tb ? tb->GetPosition() : glm::vec3(0.0f); - float da2 = glm::length2(pa - camPos); - float db2 = glm::length2(pb - camPos); - - // Primary key: distance (farther first) - if (da2 != db2) { - return da2 > db2; - } - - // Secondary key: for entities at nearly the same distance, prefer - // rendering liquid volumes before glass shells so bar glasses look - // correctly filled. This is a heuristic based on material flags. - auto classify = [this](Entity* e) { - bool hasGlass = false; - bool hasLiquid = false; - if (!e || !modelLoader) return std::pair{false, false}; - - std::string name = e->GetName(); - size_t tagPos = name.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < name.length()) { - std::string remainder = name.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { - std::string materialName = remainder.substr(nextUnderscore + 1); - if (Material* m = modelLoader->GetMaterial(materialName)) { - hasGlass = m->isGlass; - hasLiquid = m->isLiquid; - } - } - } - } - return std::pair{hasGlass, hasLiquid}; - }; - - auto [aIsGlass, aIsLiquid] = classify(a); - auto [bIsGlass, bIsLiquid] = classify(b); - - // If one is liquid and the other is glass at the same distance, - // render the liquid first (i.e., treat it as slightly farther). - if (aIsLiquid && bIsGlass && !bIsLiquid) { - return true; // a (liquid) comes before b (glass) - } - if (bIsLiquid && aIsGlass && !aIsLiquid) { - return false; // b (liquid) comes before a (glass) - } - - // Fallback to stable ordering when distances and classifications are equal. - return a < b; - }); - } - - // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE - { - vk::ImageMemoryBarrier barrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, barrier); - vk::RenderingAttachmentInfo colorAttachment{ .imageView = *opaqueSceneColorImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f}) }; - depthAttachment.imageView = *depthImageView; - depthAttachment.loadOp = 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); - if (!blockScene) { - for (const auto& uptr : entities) { - Entity* entity = uptr.get(); - if (!entity || !entity->IsActive() || blendedSet.contains(entity)) continue; - auto meshComponent = entity->GetComponent(); - if (!meshComponent) continue; - bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); - vk::raii::Pipeline* selectedPipeline = useBasic ? &graphicsPipeline : &pbrGraphicsPipeline; - vk::raii::PipelineLayout* selectedLayout = useBasic ? &pipelineLayout : &pbrPipelineLayout; - if (currentPipeline != selectedPipeline) { - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); - currentPipeline = selectedPipeline; - currentLayout = selectedLayout; - } - auto meshIt = meshResources.find(meshComponent); - auto entityIt = entityResources.find(entity); - if (meshIt == meshResources.end() || entityIt == entityResources.end()) continue; - std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; - std::array offsets = {0, 0}; - commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); - commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - updateUniformBuffer(currentFrame, entity, camera); - auto& descSets = useBasic ? entityIt->second.basicDescriptorSets : entityIt->second.pbrDescriptorSets; - if (descSets.empty() || currentFrame >= descSets.size()) continue; - if (useBasic) { - // Basic pipeline expects only set 0 - commandBuffers[currentFrame].bindDescriptorSets( - vk::PipelineBindPoint::eGraphics, - **currentLayout, - 0, - { *descSets[currentFrame] }, - {} - ); - } else { - // Opaque PBR pipeline: bind set 0 (PBR) and a valid set 1 (fallback scene color) - vk::DescriptorSet set1Opaque = *transparentFallbackDescriptorSets[currentFrame]; - commandBuffers[currentFrame].bindDescriptorSets( - vk::PipelineBindPoint::eGraphics, - **currentLayout, - 0, - { *descSets[currentFrame], set1Opaque }, - {} - ); - } - if (!useBasic) { - MaterialProperties pushConstants{}; - // Sensible defaults for entities without explicit material - pushConstants.baseColorFactor = glm::vec4(1.0f); - pushConstants.metallicFactor = 0.0f; - pushConstants.roughnessFactor = 1.0f; - pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) - pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 - pushConstants.normalTextureSet = -1; - pushConstants.occlusionTextureSet = -1; - pushConstants.emissiveTextureSet = -1; - pushConstants.alphaMask = 0.0f; - pushConstants.alphaMaskCutoff = 0.5f; - pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 1.0f; - pushConstants.hasEmissiveStrengthExtension = false; // Default entities don't have emissive strength extension - pushConstants.transmissionFactor = 0.0f; - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.glossinessFactor = 0.0f; - pushConstants.specularFactor = glm::vec3(1.0f); - // pushConstants.ior already 1.5f default - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { - std::string materialName = remainder.substr(nextUnderscore + 1); - Material* material = modelLoader->GetMaterial(materialName); - if (material) { - // Base factors - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - - // Texture set flags (-1 = no texture) - pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; - // physical descriptor: MR or SpecGloss - if (material->useSpecularGlossiness) { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; - pushConstants.glossinessFactor = material->glossinessFactor; - pushConstants.specularFactor = material->specularFactor; - } else { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; - } - pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; - pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; - pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; - - // Emissive and transmission/IOR - pushConstants.emissiveFactor = material->emissive; - pushConstants.emissiveStrength = material->emissiveStrength; - pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data - pushConstants.transmissionFactor = material->transmissionFactor; - pushConstants.ior = material->ior; - - // Alpha mask handling - pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; - } - } - } - } - } - // If no explicit MASK from a material, infer it from the baseColor texture's alpha usage - if (pushConstants.alphaMask < 0.5f) { - std::string baseColorPath; - if (meshComponent) { - if (!meshComponent->GetBaseColorTexturePath().empty()) { - baseColorPath = meshComponent->GetBaseColorTexturePath(); - } else if (!meshComponent->GetTexturePath().empty()) { - baseColorPath = meshComponent->GetTexturePath(); - } else { - baseColorPath = SHARED_DEFAULT_ALBEDO_ID; - } - } else { - baseColorPath = SHARED_DEFAULT_ALBEDO_ID; - } - // Avoid inferring MASK from the shared default albedo (semi-transparent placeholder) - if (baseColorPath != SHARED_DEFAULT_ALBEDO_ID) { - const std::string resolvedBase = ResolveTextureId(baseColorPath); - std::shared_lock texLock(textureResourcesMutex); - auto itTex = textureResources.find(resolvedBase); - if (itTex != textureResources.end() && itTex->second.alphaMaskedHint) { - pushConstants.alphaMask = 1.0f; - pushConstants.alphaMaskCutoff = 0.5f; - } - } - } - commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, { pushConstants }); - } - uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); - commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); - } - } - commandBuffers[currentFrame].endRendering(); - } - // BARRIER AND COPY - { - vk::ImageMemoryBarrier opaqueSrcBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .dstAccessMask = vk::AccessFlagBits::eTransferRead, .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, .newLayout = vk::ImageLayout::eTransferSrcOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, opaqueSrcBarrier); - vk::ImageMemoryBarrier swapchainDstBarrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eTransferWrite, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eTransferDstOptimal, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, swapchainDstBarrier); - vk::ImageCopy copyRegion{ .srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, .dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, .extent = {swapChainExtent.width, swapChainExtent.height, 1} }; - commandBuffers[currentFrame].copyImage(*opaqueSceneColorImage, vk::ImageLayout::eTransferSrcOptimal, swapChainImages[imageIndex], vk::ImageLayout::eTransferDstOptimal, copyRegion); - vk::ImageMemoryBarrier opaqueShaderBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferRead, .dstAccessMask = vk::AccessFlagBits::eShaderRead, .oldLayout = vk::ImageLayout::eTransferSrcOptimal, .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .image = *opaqueSceneColorImage, .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, opaqueShaderBarrier); - vk::ImageMemoryBarrier swapchainTargetBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferWrite, .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, swapchainTargetBarrier); - } - // PASS 2: RENDER TRANSPARENT OBJECTS TO THE SWAPCHAIN - { - 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); - 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); - - if (!blendedQueue.empty()) { - currentLayout = &pbrTransparentPipelineLayout; - - // Track currently bound pipeline so we only rebind when needed - vk::raii::Pipeline* activeTransparentPipeline = nullptr; - - for (Entity* entity : blendedQueue) { - auto meshComponent = entity->GetComponent(); - auto entityIt = entityResources.find(entity); - auto meshIt = meshResources.find(meshComponent); - if (!meshComponent || entityIt == entityResources.end() || meshIt == meshResources.end()) continue; - - // Resolve material for this entity (if any) - Material* material = nullptr; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) { - std::string materialName = remainder.substr(nextUnderscore + 1); - material = modelLoader->GetMaterial(materialName); - } - } - } - } - - // Choose pipeline: specialized glass pipeline for architectural glass, - // otherwise the generic blended PBR pipeline. - bool useGlassPipeline = material && material->isGlass; - vk::raii::Pipeline* desiredPipeline = useGlassPipeline ? &glassGraphicsPipeline : &pbrBlendGraphicsPipeline; - if (desiredPipeline != activeTransparentPipeline) { - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **desiredPipeline); - activeTransparentPipeline = desiredPipeline; - } - - std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; - std::array offsets = {0, 0}; - commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); - commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); - updateUniformBuffer(currentFrame, entity, camera); - - auto& pbrDescSets = entityIt->second.pbrDescriptorSets; - if (pbrDescSets.empty() || currentFrame >= pbrDescSets.size()) continue; - - // Bind PBR (set 0) and scene color (set 1). If primary set 1 is unavailable, use fallback. - vk::DescriptorSet set1 = transparentDescriptorSets.empty() - ? *transparentFallbackDescriptorSets[currentFrame] - : *transparentDescriptorSets[currentFrame]; - commandBuffers[currentFrame].bindDescriptorSets( - vk::PipelineBindPoint::eGraphics, - **currentLayout, - 0, - { *pbrDescSets[currentFrame], set1 }, - {} - ); - - MaterialProperties pushConstants{}; - // Sensible defaults for entities without explicit material - pushConstants.baseColorFactor = glm::vec4(1.0f); - pushConstants.metallicFactor = 0.0f; - pushConstants.roughnessFactor = 1.0f; - pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) - pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 - pushConstants.normalTextureSet = -1; - pushConstants.occlusionTextureSet = -1; - pushConstants.emissiveTextureSet = -1; - pushConstants.alphaMask = 0.0f; - pushConstants.alphaMaskCutoff = 0.5f; - pushConstants.emissiveFactor = glm::vec3(0.0f); - pushConstants.emissiveStrength = 1.0f; - pushConstants.hasEmissiveStrengthExtension = false; - pushConstants.transmissionFactor = 0.0f; - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.glossinessFactor = 0.0f; - pushConstants.specularFactor = glm::vec3(1.0f); - // pushConstants.ior already 1.5f default - if (material) { - // Base factors - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - - // Texture set flags (-1 = no texture) - pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; - if (material->useSpecularGlossiness) { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; - pushConstants.glossinessFactor = material->glossinessFactor; - pushConstants.specularFactor = material->specularFactor; - } else { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; - } - pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; - pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; - pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; - - // Emissive and transmission/IOR - pushConstants.emissiveFactor = material->emissive; - pushConstants.emissiveStrength = material->emissiveStrength; - pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data - pushConstants.transmissionFactor = material->transmissionFactor; - pushConstants.ior = material->ior; - - // Alpha mask handling - pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; - - // For bar liquids and similar volumes, we want the fill to be - // clearly visible rather than fully transmissive. For these - // materials, disable the transmission branch in the PBR shader - // and treat them as regular alpha-blended PBR surfaces. - if (material->isLiquid) { - pushConstants.transmissionFactor = 0.0f; - } - } - commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, { pushConstants }); - uint32_t instanceCountT = std::max(1u, static_cast(meshComponent->GetInstanceCount())); - commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCountT, 0, 0, 0); - } - } - - if (imguiSystem) { - imguiSystem->Render(commandBuffers[currentFrame], currentFrame); - } - commandBuffers[currentFrame].endRendering(); - } - - // Final layout transition and present - vk::ImageMemoryBarrier presentBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, .dstAccessMask = vk::AccessFlagBits::eNone, .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, .newLayout = vk::ImageLayout::ePresentSrcKHR, .image = swapChainImages[imageIndex], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1} }; - commandBuffers[currentFrame].pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe, {}, {}, {}, presentBarrier); - commandBuffers[currentFrame].end(); - std::array waitSems = { *imageAvailableSemaphores[currentFrame], *uploadsTimeline }; - std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader }; - uint64_t uploadsValueToWait = uploadTimelineLastSubmitted.load(std::memory_order_relaxed); - std::array waitValues = { 0ull, uploadsValueToWait }; - vk::TimelineSemaphoreSubmitInfo timelineWaitInfo{ .waitSemaphoreValueCount = static_cast(waitValues.size()), .pWaitSemaphoreValues = waitValues.data() }; - vk::SubmitInfo submitInfo{ .pNext = &timelineWaitInfo, .waitSemaphoreCount = static_cast(waitSems.size()), .pWaitSemaphores = waitSems.data(), .pWaitDstStageMask = waitStages.data(), .commandBufferCount = 1, .pCommandBuffers = &*commandBuffers[currentFrame], .signalSemaphoreCount = 1, .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex] }; - if (framebufferResized.load(std::memory_order_relaxed)) { - vk::SubmitInfo emptySubmit{}; - { std::lock_guard lock(queueMutex); graphicsQueue.submit(emptySubmit, *inFlightFences[currentFrame]); } - recreateSwapChain(); - return; - } - { std::lock_guard lock(queueMutex); graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); } - vk::PresentInfoKHR presentInfo{ .waitSemaphoreCount = 1, .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], .swapchainCount = 1, .pSwapchains = &*swapChain, .pImageIndices = &imageIndex }; - try { - std::lock_guard lock(queueMutex); - result.result = presentQueue.presentKHR(presentInfo); - } catch (const vk::OutOfDateKHRError&) { - framebufferResized.store(true, std::memory_order_relaxed); - } - if (result.result == vk::Result::eErrorOutOfDateKHR || result.result == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { - framebufferResized.store(false, std::memory_order_relaxed); - recreateSwapChain(); - } else if (result.result != vk::Result::eSuccess) { - throw std::runtime_error("Failed to present swap chain image"); - } - - 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); + + static bool firstRenderLogged = false; + if (!firstRenderLogged) + { + std::cout << "Entering main render loop - application is running successfully!" << std::endl; + firstRenderLogged = true; + } + + 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; + + // Wait for the previous frame's work on this frame slot to complete + if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + { + std::cerr << "Warning: Failed to wait for fence on frame " << currentFrame << std::endl; + return; + } + + // Reset the fence immediately after successful wait, before any new work + 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. + ProcessPendingMeshUploads(); + + // 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; + } + } + } + + // 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). + if (rayQueryEnabled && accelerationStructureEnabled) + { + size_t readyRenderableCount = 0; + size_t readyUniqueMeshCount = 0; + { + std::map meshToBLASProbe; + for (const auto &uptr : entities) + { + Entity *e = uptr.get(); + 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(); + } + if (asOpportunisticRebuildEnabled && !asFrozen && (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 + if (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 + if (asBuildRequested.load(std::memory_order_acquire)) + { + // Defer TLAS/BLAS build while the scene is loading to avoid partial builds (e.g., only animated fans) + if (IsLoading()) + { + // Keep the request flag set; we'll build once loading completes + static uint32_t asDeferredLoadingCounter = 0; + if ((asDeferredLoadingCounter++ % 120u) == 0u) + { + std::cout << "AS build deferred: scene still loading" << std::endl; + } + } + else if (asFrozen && !asDevOverrideAllowRebuild) + { + // 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); + } + 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; + { + std::map meshToBLASProbe; + for (const auto &uptr : entities) + { + Entity *e = uptr.get(); + 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()) + continue; + const auto &res = it->second; + // STRICT readiness here too: uploads finished + 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(); + } + const double readiness = (totalRenderableEntities > 0) ? static_cast(readyRenderableCount) / static_cast(totalRenderableEntities) : 0.0; + const double buildThreshold = 0.95; // build only when ~full scene is ready + if (readiness < buildThreshold && !asDevOverrideAllowRebuild) + { + static uint32_t asDeferredReadinessCounter = 0; + if ((asDeferredReadinessCounter++ % 120u) == 0u) + { + std::cout << "AS build deferred: readiness " << readyRenderableCount << "/" << totalRenderableEntities + << " entities (" << static_cast(readiness * 100.0) << "%), uniqueMeshesReady=" + << readyUniqueMeshCount << std::endl; + } + // Keep the request flag set; try again next frame + } + else + { + // CRITICAL: Wait for ALL GPU work to complete BEFORE building AS. + // External synchronization required (VVL): serialize against queue submits/present. + // This ensures no command buffers are still using vertex/index buffers that the AS build will reference. + WaitIdle(); + + if (buildAccelerationStructures(entities)) + { + asBuildRequested.store(false, std::memory_order_release); + // Freeze only when the built TLAS is "full" (>=95% of static opaque renderables) + if (asFreezeAfterFullBuild) + { + const double threshold = 0.95; + if (totalRenderableEntities > 0 && static_cast(lastASBuiltInstanceCount) >= threshold * static_cast(totalRenderableEntities)) + { + asFrozen = true; + std::cout << "AS frozen after full build (instances=" << lastASBuiltInstanceCount + << "/" << totalRenderableEntities << ")" << std::endl; + } + else + { + std::cout << "AS not frozen yet (built instances=" << lastASBuiltInstanceCount + << ", total renderables=" << totalRenderableEntities << ")" << std::endl; + } + } + // One-line TLAS summary with device address + if (*tlasStructure.handle) + { + if (IsRayQueryStaticOnly()) + { + std::cout << "TLAS ready (static-only): instances=" << lastASBuiltInstanceCount + << ", BLAS=" << lastASBuiltBLASCount + << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << std::endl; + } + else + { + std::cout << "TLAS ready: instances=" << lastASBuiltInstanceCount + << ", BLAS=" << lastASBuiltBLASCount + << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << std::endl; + } + } + } + else + { + 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. + ProcessDirtyDescriptorsForFrame(currentFrame); + + // Safe point pre-pass: ensure descriptor sets exist for all visible entities this frame + // and initialize only binding 0 (UBO) for the current frame if not already done. + { + uint32_t entityProcessCount = 0; + for (const auto &uptr : entities) + { + Entity *entity = uptr.get(); + if (!entity || !entity->IsActive()) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + continue; + + // Update watchdog every 100 entities to prevent false hang detection during heavy descriptor creation + entityProcessCount++; + if (entityProcessCount % 100 == 0) + { + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + } + + // Determine a reasonable base texture path for initial descriptor writes + std::string texPath = meshComponent->GetBaseColorTexturePath(); + if (texPath.empty()) + texPath = meshComponent->GetTexturePath(); + + // Create descriptor sets on demand if missing + if (entityIt->second.basicDescriptorSets.empty()) + { + createDescriptorSets(entity, texPath, /*usePBR=*/false); + } + if (entityIt->second.pbrDescriptorSets.empty()) + { + createDescriptorSets(entity, texPath, /*usePBR=*/true); + } + + // Ensure ONLY binding 0 (UBO) is written for the CURRENT frame's PBR set once. + // Avoid touching image bindings here to keep per-frame descriptor churn minimal. + updateDescriptorSetsForFrame(entity, + texPath, + /*usePBR=*/true, + currentFrame, + /*imagesOnly=*/false, + /*uboOnly=*/true); + + // Cold-initialize image bindings for CURRENT frame once to avoid per-frame black flashes. + // This writes PBR b1..b5 and Basic b1 with either real textures or shared defaults. + // It does not touch UBO (handled above). + // PBR images + if (entityIt->second.pbrImagesWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + entityIt->second.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (!entityIt->second.pbrImagesWritten[currentFrame]) + { + updateDescriptorSetsForFrame(entity, + texPath, + /*usePBR=*/true, + currentFrame, + /*imagesOnly=*/true, + /*uboOnly=*/false); + entityIt->second.pbrImagesWritten[currentFrame] = true; + } + // Basic images + if (entityIt->second.basicImagesWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + entityIt->second.basicImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (!entityIt->second.basicImagesWritten[currentFrame]) + { + updateDescriptorSetsForFrame(entity, + texPath, + /*usePBR=*/false, + currentFrame, + /*imagesOnly=*/true, + /*uboOnly=*/false); + entityIt->second.basicImagesWritten[currentFrame] = true; + } + } + } + + // 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)) + { + std::vector ops; + { + std::lock_guard lk(pendingDescMutex); + ops.swap(pendingDescOps); + descriptorRefreshPending.store(false, std::memory_order_relaxed); + } + for (auto &op : ops) + { + 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); + } + } + } + + // 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]; + } + + // CRITICAL FIX: DO NOT call refreshPBRForwardPlusBindingsForFrame every frame! + // 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 + // We must provide a semaphore to acquireNextImage that will be signaled when the image is ready. + // Use currentFrame to cycle through available semaphores (one per frame-in-flight). + // After acquire, we'll use imageIndex to select semaphores for submit/present. + uint32_t acquireSemaphoreIndex = currentFrame % static_cast(imageAvailableSemaphores.size()); + + uint32_t imageIndex; + vk::ResultValue result{{}, 0}; + try + { + result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[acquireSemaphoreIndex]); + } + catch (const vk::OutOfDateKHRError &) + { + // 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 = result.value; + + if (result.result == vk::Result::eErrorOutOfDateKHR || result.result == 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 (result.result != 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) + { + updateRayQueryDescriptorSets(currentFrame, entities); + } + } + + 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; + } + + // Extract lights for this frame (needed by both ray query and rasterization) + // 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) + { + if (L.type == ExtractedLight::Type::Emissive) + { + lightsSubset.push_back(L); + if (lightsSubset.size() >= MAX_ACTIVE_LIGHTS) + break; + } + } + } + auto lightCountF = static_cast(lightsSubset.size()); + lastFrameLightCount = lightCountF; + if (!lightsSubset.empty()) + { + updateLightStorageBuffer(currentFrame, lightsSubset); + } + + // 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 – present a diagnostic frame from the ray-query path to avoid + // accidentally showing rasterized content. Fill swapchain with a distinct color. + // 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{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 + // In static-only mode, skip refit to keep TLAS immutable + if (!IsRayQueryStaticOnly()) + { + // If animation updated transforms this frame, refit TLAS instead of rebuilding + // This prevents wiping TLAS contents to only animated instances + refitTopLevelAS(entities); + } + + // Update descriptors for this frame. If it fails (e.g., stale/invalid sets), skip ray query safely. + if (!updateRayQueryDescriptorSets(currentFrame, entities)) + { + std::cerr << "Ray Query descriptor update failed; skipping ray query this frame\n"; + } + else + { + // 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); + + // CRITICAL: Update dedicated ray query UBO with camera matrices + // 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 ~0.25 in the main pass; keep Ray Query consistent.) + ubo.scaleIBLAmbient = 0.25f; + // 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; + // 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; + 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 != nullptr) + { + 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; + std::vector blendedQueue; + std::unordered_set blendedSet; + + // 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 + // Skip rendering the UI when loading or if ImGuiSystem already called Render() during NewFrame(). + // This prevents calling ImGui::Begin() after ImGui::Render() has been called in the same frame, + // which would violate ImGui's frame lifecycle and trigger assertion failures. + if (imguiSystem && !IsLoading() && !imguiSystem->IsFrameRendered()) + { + 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(); + } + } + } + } + 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 == nullptr || forwardPlusDescriptorSetLayout == nullptr || forwardPlusPerFrame.empty()) + { + createForwardPlusPipelinesAndResources(); + } + if (depthPrepassPipeline == nullptr) + { + createDepthPrepassPipeline(); + } + } + + // 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; + } + 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); + } + } + if (enablePlanarReflections) + { + ImGui::SliderFloat("Reflection intensity", &reflectionIntensity, 0.0f, 2.0f, "%.2f"); + } + } + + // === 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 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:"); + 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. + // Previously this always executed, but now we skip it when ray query mode successfully renders. + if (!rayQueryRenderedThisFrame) + { + // Prepare frustum once per frame + FrustumPlanes frustum{}; + const bool doCulling = enableFrustumCulling && camera; + if (doCulling) + { + const glm::mat4 vp = camera->GetProjectionMatrix() * camera->GetViewMatrix(); + frustum = extractFrustumPlanes(vp); + } + + lastCullingVisibleCount = 0; + lastCullingCulledCount = 0; + + // Optional: render planar reflections first + if (enablePlanarReflections) + { + // Default plane: Y=0 (upwards normal) — replace with component-driven plane later + glm::vec4 planeWS(0.0f, 1.0f, 0.0f, 0.0f); + renderReflectionPass(commandBuffers[currentFrame], planeWS, camera, entities); + } + + for (const auto &uptr : entities) + { + Entity *entity = uptr.get(); + if (!entity || !entity->IsActive()) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + + // Frustum culling + if (doCulling && meshComponent->HasLocalAABB()) + { + auto *tc = entity->GetComponent(); + const glm::mat4 model = tc ? tc->GetModelMatrix() : glm::mat4(1.0f); + glm::vec3 wmin, wmax; + transformAABB(model, meshComponent->GetLocalAABBMin(), meshComponent->GetLocalAABBMax(), wmin, wmax); + if (!aabbIntersectsFrustum(wmin, wmax, frustum)) + { + lastCullingCulledCount++; + continue; // culled early + } + } + lastCullingVisibleCount++; + bool useBlended = false; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) + { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + Material *material = modelLoader->GetMaterial(materialName); + // Classify as blended only for true alpha-blend materials, glass or liquids, or high transmission. + // This avoids shunting most opaque materials into the transparent pass (which skips the off-screen buffer). + bool isBlendedMat = false; + if (material) + { + bool alphaBlend = (material->alphaMode == "BLEND"); + bool highTransmission = (material->transmissionFactor > 0.2f); + bool glassLike = material->isGlass; + bool liquidLike = material->isLiquid; + isBlendedMat = alphaBlend || highTransmission || glassLike || liquidLike; + } + if (isBlendedMat) + { + useBlended = true; + } + } + } + } + } + + // Ensure all entities are considered regardless of reflections setting. + // Previous diagnostic mode skipped non-glass when reflections were ON, which could + // result in frames with few/no draws and visible black flashes. We no longer filter here. + + // Distance-based LOD: approximate screen-space size of entity's bounds + if (enableDistanceLOD && camera && meshComponent && meshComponent->HasLocalAABB()) + { + auto *tc = entity->GetComponent(); + const glm::mat4 model = tc ? tc->GetModelMatrix() : glm::mat4(1.0f); + glm::vec3 localMin = meshComponent->GetLocalAABBMin(); + glm::vec3 localMax = meshComponent->GetLocalAABBMax(); + // Compute world AABB bounds + glm::vec3 wmin, wmax; + transformAABB(model, localMin, localMax, wmin, wmax); + glm::vec3 center = 0.5f * (wmin + wmax); + glm::vec3 extents = 0.5f * (wmax - wmin); + float radius = glm::length(extents); + if (radius > 0.0f) + { + glm::vec4 centerVS4 = camera->GetViewMatrix() * glm::vec4(center, 1.0f); + float z = std::abs(centerVS4.z); + if (z > 1e-3f) + { + float fov = glm::radians(camera->GetFieldOfView()); + float pixelRadius = (radius * static_cast(swapChainExtent.height)) / (z * 2.0f * std::tan(fov * 0.5f)); + float pixelDiameter = pixelRadius * 2.0f; + float threshold = useBlended ? lodPixelThresholdTransparent : lodPixelThresholdOpaque; + if (pixelDiameter < threshold) + { + // Too small to matter; skip adding to draw queues + continue; + } + } + } + } + if (useBlended) + { + blendedQueue.push_back(entity); + blendedSet.insert(entity); + } + } + + // Sort transparent entities back-to-front for correct blending of nested glass/liquids + if (!blendedQueue.empty()) + { + // Sort by squared distance from the camera in world space. + // Farther objects must be rendered first so that nearer glass correctly + // appears in front (standard back-to-front transparency ordering). + glm::vec3 camPos = camera ? camera->GetPosition() : glm::vec3(0.0f); + std::ranges::sort(blendedQueue, [this, camPos](Entity *a, Entity *b) { + auto *ta = (a ? a->GetComponent() : nullptr); + auto *tb = (b ? b->GetComponent() : nullptr); + glm::vec3 pa = ta ? ta->GetPosition() : glm::vec3(0.0f); + glm::vec3 pb = tb ? tb->GetPosition() : glm::vec3(0.0f); + float da2 = glm::length2(pa - camPos); + float db2 = glm::length2(pb - camPos); + + // Primary key: distance (farther first) + if (da2 != db2) + { + return da2 > db2; + } + + // Secondary key: for entities at nearly the same distance, prefer + // rendering liquid volumes before glass shells so bar glasses look + // correctly filled. This is a heuristic based on material flags. + auto classify = [this](Entity *e) { + bool hasGlass = false; + bool hasLiquid = false; + if (!e || !modelLoader) + return std::pair{false, false}; + + const std::string &name = e->GetName(); + size_t tagPos = name.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < name.length()) + { + std::string remainder = name.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + if (Material *m = modelLoader->GetMaterial(materialName)) + { + hasGlass = m->isGlass; + hasLiquid = m->isLiquid; + } + } + } + } + return std::pair{hasGlass, hasLiquid}; + }; + + auto [aIsGlass, aIsLiquid] = classify(a); + auto [bIsGlass, bIsLiquid] = classify(b); + + // If one is liquid and the other is glass at the same distance, + // render the liquid first (i.e., treat it as slightly farther). + if (aIsLiquid && bIsGlass && !bIsLiquid) + { + return true; // a (liquid) comes before b (glass) + } + if (bIsLiquid && aIsGlass && !aIsLiquid) + { + return false; // b (liquid) comes before a (glass) + } + + // Fallback to stable ordering when distances and classifications are equal. + return a < b; + }); + } + + // 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) + { + // Build list of non-blended entities + std::vector opaqueEntities; + opaqueEntities.reserve(entities.size()); + for (const auto &uptr : entities) + { + Entity *entity = uptr.get(); + if (!entity || !entity->IsActive() || blendedSet.contains(entity)) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + opaqueEntities.push_back(entity); + } + + if (!opaqueEntities.empty()) + { + // Transition depth image for attachment write (Sync2) + vk::ImageMemoryBarrier2 depthBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .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 == nullptr)) + { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *depthPrepassPipeline); + } + + for (Entity *entity : opaqueEntities) + { + auto meshComponent = entity->GetComponent(); + auto entityIt = entityResources.find(entity); + auto meshIt = meshResources.find(meshComponent); + if (!meshComponent || entityIt == entityResources.end() || meshIt == meshResources.end()) + continue; + + // Skip alpha-masked geometry in the depth pre-pass so that depth is not written + // where fragments would be discarded by alpha test. These will write depth during + // the opaque color pass using the standard opaque pipeline. + bool isAlphaMasked = false; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) + { + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + if (Material *m = modelLoader->GetMaterial(materialName)) + { + isAlphaMasked = (m->alphaMode == "MASK"); + } + } + } + } + } + // Fallback: infer mask from baseColor texture alpha usage hint + if (!isAlphaMasked) + { + std::string baseColorPath; + if (meshComponent) + { + if (!meshComponent->GetBaseColorTexturePath().empty()) + { + baseColorPath = meshComponent->GetBaseColorTexturePath(); + } + else if (!meshComponent->GetTexturePath().empty()) + { + baseColorPath = meshComponent->GetTexturePath(); + } + } + if (!baseColorPath.empty()) + { + const std::string resolvedBase = ResolveTextureId(baseColorPath); + std::shared_lock texLock(textureResourcesMutex); + auto itTex = textureResources.find(resolvedBase); + if (itTex != textureResources.end() && itTex->second.alphaMaskedHint) + { + isAlphaMasked = true; + } + } + } + if (isAlphaMasked) + { + continue; // do not write depth for masked foliage in pre-pass + } + + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + updateUniformBuffer(currentFrame, entity, camera); + + auto &descSets = entityIt->second.pbrDescriptorSets; + if (descSets.empty() || currentFrame >= descSets.size()) + continue; + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *pbrPipelineLayout, 0, {*descSets[currentFrame]}, {}); + uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(meshIt->second.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); + + // Forward+ per-frame debug printing removed + + dispatchForwardPlus(commandBuffers[currentFrame], tilesX, tilesY, forwardPlusSlicesZ); + // Forward+ debug dumps and tile header prints removed + } + + // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE + // Transition off-screen color from last frame's sampling to attachment write (Sync2) + vk::ImageMemoryBarrier2 oscToColor2{.srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .srcAccessMask = vk::AccessFlagBits2::eShaderRead, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = *opaqueSceneColorImage, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; + vk::DependencyInfo depOscToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &oscToColor2}; + commandBuffers[currentFrame].pipelineBarrier2(depOscToColor); + // Clear the off-screen target at the start of opaque rendering to a neutral black background + vk::RenderingAttachmentInfo colorAttachment{.imageView = *opaqueSceneColorImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})}; + depthAttachment.imageView = *depthImageView; + // Load depth only if we actually performed a pre-pass (and not in opaque-only debug which intentionally ignores transparency ordering) + 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 &uptr : entities) + { + Entity *entity = uptr.get(); + if (!entity || !entity->IsActive() || (blendedSet.contains(entity))) + continue; + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + continue; + bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); + vk::raii::Pipeline *selectedPipeline = nullptr; + vk::raii::PipelineLayout *selectedLayout = nullptr; + if (useBasic) + { + selectedPipeline = &graphicsPipeline; + selectedLayout = &pipelineLayout; + } + else + { + // Determine if this entity uses alpha masking so we can bypass the post-prepass + // read-only pipeline and use the normal depth-writing opaque pipeline instead. + bool isAlphaMaskedOpaque = false; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) + { + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + if (Material *m = modelLoader->GetMaterial(materialName)) + { + isAlphaMaskedOpaque = (m->alphaMode == "MASK"); + } + } + } + } + } + // Fallback based on texture hint if material flag not set + if (!isAlphaMaskedOpaque) + { + std::string baseColorPath; + if (meshComponent) + { + if (!meshComponent->GetBaseColorTexturePath().empty()) + { + baseColorPath = meshComponent->GetBaseColorTexturePath(); + } + else if (!meshComponent->GetTexturePath().empty()) + { + baseColorPath = meshComponent->GetTexturePath(); + } + } + if (!baseColorPath.empty()) + { + const std::string resolvedBase = ResolveTextureId(baseColorPath); + std::shared_lock texLock(textureResourcesMutex); + auto itTex = textureResources.find(resolvedBase); + if (itTex != textureResources.end() && itTex->second.alphaMaskedHint) + { + isAlphaMaskedOpaque = true; + } + } + } + // If masked, we need depth writes with alpha test; otherwise, after-prepass read-only is fine. + if (isAlphaMaskedOpaque) + { + selectedPipeline = &pbrGraphicsPipeline; // writes depth, compare Less + } + else + { + selectedPipeline = didOpaqueDepthPrepass && (pbrPrepassGraphicsPipeline != nullptr) ? &pbrPrepassGraphicsPipeline : &pbrGraphicsPipeline; + } + selectedLayout = &pbrPipelineLayout; + } + if (currentPipeline != selectedPipeline) + { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + auto meshIt = meshResources.find(meshComponent); + auto entityIt = entityResources.find(entity); + if (meshIt == meshResources.end() || entityIt == entityResources.end()) + continue; + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + updateUniformBuffer(currentFrame, entity, camera); + auto *descSetsPtr = useBasic ? &entityIt->second.basicDescriptorSets : &entityIt->second.pbrDescriptorSets; + if (descSetsPtr->empty() || currentFrame >= descSetsPtr->size()) + { + // Never create or update descriptor sets during command buffer recording. + // Mark this entity dirty so the safe point will initialize its descriptors next frame. + MarkEntityDescriptorsDirty(entity); + static bool printedOnceMissingSets = false; + if (!printedOnceMissingSets) + { + std::cerr << "[Descriptors] Descriptor sets missing for '" << entity->GetName() << "' — deferring to safe point, draw skipped this frame" << std::endl; + printedOnceMissingSets = true; + } + continue; + } + // (binding of descriptor sets happens below using descSetsPtr for the chosen pipeline) + if (!useBasic) + { + MaterialProperties pushConstants{}; + // Sensible defaults for entities without explicit material + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 0.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) + pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + pushConstants.hasEmissiveStrengthExtension = false; // Default entities don't have emissive strength extension + pushConstants.transmissionFactor = 0.0f; + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.glossinessFactor = 0.0f; + pushConstants.specularFactor = glm::vec3(1.0f); + // pushConstants.ior already 1.5f default + // If we don't resolve a material below, still show emissive textures if bound at set 5 + if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) + { + pushConstants.emissiveTextureSet = 0; + pushConstants.emissiveFactor = glm::vec3(1.0f); + pushConstants.emissiveStrength = 1.0f; + pushConstants.hasEmissiveStrengthExtension = false; + } + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) + { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + Material *material = modelLoader->GetMaterial(materialName); + if (material) + { + // Base factors + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + + // Texture set flags (-1 = no texture) + pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; + // physical descriptor: MR or SpecGloss + if (material->useSpecularGlossiness) + { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.specularFactor = material->specularFactor; + } + else + { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; + pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; + pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; + + // Emissive and transmission/IOR + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + // Heuristic: consider emissive strength extension present when strength != 1.0 + pushConstants.hasEmissiveStrengthExtension = (std::abs(material->emissiveStrength - 1.0f) > 1e-6f); + pushConstants.transmissionFactor = material->transmissionFactor; + pushConstants.ior = material->ior; + + // Alpha mask handling + pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } + } + } + } + } + // If no explicit MASK from a material, infer it from the baseColor texture's alpha usage + if (pushConstants.alphaMask < 0.5f) + { + std::string baseColorPath; + if (meshComponent) + { + if (!meshComponent->GetBaseColorTexturePath().empty()) + { + baseColorPath = meshComponent->GetBaseColorTexturePath(); + } + else if (!meshComponent->GetTexturePath().empty()) + { + baseColorPath = meshComponent->GetTexturePath(); + } + else + { + baseColorPath = SHARED_DEFAULT_ALBEDO_ID; + } + } + else + { + baseColorPath = SHARED_DEFAULT_ALBEDO_ID; + } + // Avoid inferring MASK from the shared default albedo (semi-transparent placeholder) + if (baseColorPath != SHARED_DEFAULT_ALBEDO_ID) + { + const std::string resolvedBase = ResolveTextureId(baseColorPath); + std::shared_lock texLock(textureResourcesMutex); + auto itTex = textureResources.find(resolvedBase); + if (itTex != textureResources.end() && itTex->second.alphaMaskedHint) + { + pushConstants.alphaMask = 1.0f; + pushConstants.alphaMaskCutoff = 0.5f; + } + } + } + commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, {pushConstants}); + } + // Bind descriptor sets for the selected pipeline + if (useBasic) + { + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **selectedLayout, + 0, + {*(*descSetsPtr)[currentFrame]}, + {}); + } + else + { + // Opaque PBR binds set0 (PBR) and set1 (scene color fallback for transparency path, not used here but layout expects it) + vk::DescriptorSet set1Opaque = *transparentFallbackDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **selectedLayout, + 0, + {*(*descSetsPtr)[currentFrame], set1Opaque}, + {}); + } + uint32_t instanceCount = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(meshIt->second.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 = *opaqueSceneColorImage, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; + vk::DependencyInfo depOpaqueToSample{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &opaqueToSample2}; + commandBuffers[currentFrame].pipelineBarrier2(depOpaqueToSample); + + // Make the swapchain image ready for color attachment output and clear it (Sync2) + vk::ImageMemoryBarrier2 swapchainToColor2{.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe, + .srcAccessMask = vk::AccessFlagBits2::eNone, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .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 != nullptr) + { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, *compositePipeline); + } + vk::Viewport viewport2(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport2); + vk::Rect2D scissor2({0, 0}, swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor2); + + // Bind descriptor set 0 for the composite (reuse transparent descriptor set which points to off-screen color) + vk::DescriptorSet setComposite = transparentDescriptorSets.empty() ? *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 (!blendedQueue.empty()) + { + currentLayout = &pbrTransparentPipelineLayout; + + // Track currently bound pipeline so we only rebind when needed + vk::raii::Pipeline *activeTransparentPipeline = nullptr; + + for (Entity *entity : blendedQueue) + { + auto meshComponent = entity->GetComponent(); + auto entityIt = entityResources.find(entity); + auto meshIt = meshResources.find(meshComponent); + if (!meshComponent || entityIt == entityResources.end() || meshIt == meshResources.end()) + continue; + + // Resolve material for this entity (if any) + Material *material = nullptr; + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + { + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) + { + size_t afterTag = tagPos + std::string("_Material_").size(); + if (afterTag < entityName.length()) + { + // Entity name format: "modelName_Material__" + // Find the next underscore after the material index to get the actual material name + std::string remainder = entityName.substr(afterTag); + size_t nextUnderscore = remainder.find('_'); + if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) + { + std::string materialName = remainder.substr(nextUnderscore + 1); + material = modelLoader->GetMaterial(materialName); + } + } + } + } + + // Choose pipeline: specialized glass pipeline for architectural glass, + // otherwise the generic blended PBR pipeline. + bool useGlassPipeline = material && material->isGlass; + vk::raii::Pipeline *desiredPipeline = useGlassPipeline ? &glassGraphicsPipeline : &pbrBlendGraphicsPipeline; + if (desiredPipeline != activeTransparentPipeline) + { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **desiredPipeline); + activeTransparentPipeline = desiredPipeline; + } + + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + updateUniformBuffer(currentFrame, entity, camera); + + auto &pbrDescSets = entityIt->second.pbrDescriptorSets; + if (pbrDescSets.empty() || currentFrame >= pbrDescSets.size()) + continue; + + // Bind PBR (set 0) and scene color (set 1). If primary set 1 is unavailable, use fallback. + vk::DescriptorSet set1 = transparentDescriptorSets.empty() ? *transparentFallbackDescriptorSets[currentFrame] : *transparentDescriptorSets[currentFrame]; + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + **currentLayout, + 0, + {*pbrDescSets[currentFrame], set1}, + {}); + + MaterialProperties pushConstants{}; + // Sensible defaults for entities without explicit material + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 0.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 0; // sample bound baseColor (falls back to shared default if none) + pushConstants.physicalDescriptorTextureSet = 0; // default to sampling metallic-roughness on binding 2 + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + pushConstants.hasEmissiveStrengthExtension = false; + pushConstants.transmissionFactor = 0.0f; + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.glossinessFactor = 0.0f; + pushConstants.specularFactor = glm::vec3(1.0f); + // pushConstants.ior already 1.5f default + if (material) + { + // Base factors + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + + // Texture set flags (-1 = no texture) + pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; + if (material->useSpecularGlossiness) + { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.specularFactor = material->specularFactor; + } + else + { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; + } + pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; + pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; + pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; + + // Emissive and transmission/IOR + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data + pushConstants.transmissionFactor = material->transmissionFactor; + pushConstants.ior = material->ior; + + // Alpha mask handling + pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + + // For bar liquids and similar volumes, we want the fill to be + // clearly visible rather than fully transmissive. For these + // materials, disable the transmission branch in the PBR shader + // and treat them as regular alpha-blended PBR surfaces. + if (material->isLiquid) + { + pushConstants.transmissionFactor = 0.0f; + } + } + commandBuffers[currentFrame].pushConstants(**currentLayout, vk::ShaderStageFlagBits::eFragment, 0, {pushConstants}); + uint32_t instanceCountT = std::max(1u, static_cast(meshComponent->GetInstanceCount())); + commandBuffers[currentFrame].drawIndexed(meshIt->second.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) + { + // 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, + .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}; + try + { + std::lock_guard lock(queueMutex); + result.result = presentQueue.presentKHR(presentInfo); + } + catch (const vk::OutOfDateKHRError &) + { + framebufferResized.store(true, std::memory_order_relaxed); + } + if (result.result == vk::Result::eErrorOutOfDateKHR || result.result == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) + { + framebufferResized.store(false, std::memory_order_relaxed); + recreateSwapChain(); + } + else if (result.result != 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/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index cc9fadf7..edd7dd7a 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1,14 +1,31 @@ -#include "renderer.h" -#include "model_loader.h" +/* 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 +#include #include +#include +#include // stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. @@ -19,2299 +36,3918 @@ // 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_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__"; +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(); - - // Create depth image using memory pool - auto [depthImg, depthImgAllocation] = createImagePooled( - swapChainExtent.width, - swapChainExtent.height, - depthFormat, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eDepthStencilAttachment, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - depthImage = std::move(depthImg); - depthImageAllocation = std::move(depthImgAllocation); - - // Create depth image view - depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); - - // Transition depth image layout - transitionImageLayout( - *depthImage, - depthFormat, - vk::ImageLayout::eUndefined, - vk::ImageLayout::eDepthStencilAttachmentOptimal - ); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create depth resources: " << e.what() << std::endl; - return false; - } +bool Renderer::createDepthResources() +{ + try + { + // Find depth format + vk::Format depthFormat = findDepthFormat(); + + // Create depth image using memory pool + auto [depthImg, depthImgAllocation] = createImagePooled( + swapChainExtent.width, + swapChainExtent.height, + depthFormat, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + depthImage = std::move(depthImg); + depthImageAllocation = std::move(depthImgAllocation); + + // Create depth image view + depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); + + // Transition depth image layout + transitionImageLayout( + *depthImage, + depthFormat, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthStencilAttachmentOptimal); + + 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; - } +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.find(".ktx2") != std::string::npos; - - // 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 { +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.find(".ktx2") != std::string::npos; + + // 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)) { + 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.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) { - 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 { + }; + // 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.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) + { + 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)) { + 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 + 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.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) { - 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; if so, transcode to RGBA32 - wasTranscoded = ktxTexture2_NeedsTranscoding(ktxTex); - if (wasTranscoded) { - result = ktxTexture2_TranscodeBasis(ktxTex, KTX_TTF_RGBA32, 0); - if (result != KTX_SUCCESS) { - std::cerr << "Failed to transcode KTX2 BasisU texture to RGBA32: " << resolvedPath << " (error: " << result << ")" << std::endl; - ktxTexture_Destroy((ktxTexture*)ktxTex); - return false; - } - } - - texWidth = ktxTex->baseWidth; - texHeight = ktxTex->baseHeight; - texChannels = 4; // logical channels; compressed size handled below - // Disable mipmapping for now - memory pool only supports single mip level - // TODO: Implement proper mipmap support in memory pool - mipLevels = 1; - - // Calculate size for base level only (use libktx for correct size incl. compression) - imageSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); - - // Create single copy region for base level only - copyRegions.push_back({ - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = { - .aspectMask = vk::ImageAspectFlagBits::eColor, - .mipLevel = 0, - .baseArrayLayer = 0, - .layerCount = 1 - }, - .imageOffset = {0, 0, 0}, - .imageExtent = {static_cast(texWidth), static_cast(texHeight), 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 KTX2 texture data for base level only (level 0), regardless of transcode target - ktx_size_t offset = 0; - ktxTexture_GetImageOffset((ktxTexture*)ktxTex, 0, 0, 0, &offset); - const void* levelData = ktxTexture_GetData(reinterpret_cast(ktxTex)) + offset; - size_t levelSize = ktxTexture_GetImageSize((ktxTexture*)ktxTex, 0); - memcpy(data, levelData, levelSize); - } 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 { - // Transcoded to RGBA32; choose SRGB/UNORM by heuristic - textureFormat = wantSRGB ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm; - // We have CPU-visible RGBA data in 'levelData' above; scan alpha for masking hint - if (ktxTex) { - ktx_size_t offsetScan = 0; - ktxTexture_GetImageOffset((ktxTexture*)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((ktxTexture*)ktxTex); - ktxTex = nullptr; - } - - // Create texture image using memory pool - auto [textureImg, textureImgAllocation] = createImagePooled( - texWidth, - texHeight, - textureFormat, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - resources.textureImage = std::move(textureImg); - resources.textureImageAllocation = std::move(textureImgAllocation); - - // GPU upload for this texture - uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, copyRegions, mipLevels); - - // 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; - } + }; + // 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.size() > key.size() && fname.rfind(key) == fname.size() - key.size()) + { + 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((ktxTexture *) 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 != vk::FormatFeatureFlags{}) + { + textureFormat = wantSRGB2 ? vk::Format::eBc7SrgbBlock : vk::Format::eBc7UnormBlock; + } + else if (physicalDevice.getFormatProperties(vk::Format::eBc3UnormBlock).optimalTilingFeatures != vk::FormatFeatureFlags{}) + { + textureFormat = wantSRGB2 ? vk::Format::eBc3SrgbBlock : vk::Format::eBc3UnormBlock; + } + else if (physicalDevice.getFormatProperties(vk::Format::eBc1RgbUnormBlock).optimalTilingFeatures != vk::FormatFeatureFlags{}) + { + 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((ktxTexture *) 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((ktxTexture *) 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()}; + } + // Decide mip count and usage (cap to limit to reduce VRAM) + // If KTX2 provided mip levels, use them as-is. Otherwise, optionally generate for uncompressed RGBA. + bool needGenerateMips = false; + if (!isKtx2) + { + if (textureFormat == vk::Format::eR8G8B8A8Srgb || textureFormat == vk::Format::eR8G8B8A8Unorm) + { + uint32_t fullMips = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; + mipLevels = std::max(1u, std::min(fullMips, maxAutoGeneratedMipLevels)); + needGenerateMips = (mipLevels > 1); + } + else + { + mipLevels = 1; + } + } + // usage flags: only require TRANSFER_SRC when we intend to generate mips on GPU + vk::ImageUsageFlags usageFlags = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + if (needGenerateMips) + { + usageFlags |= vk::ImageUsageFlagBits::eTransferSrc; // needed for blit generation + } + + // 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); + + // Generate mip chain if requested (only for uncompressed RGBA textures not coming from KTX2) + if (!isKtx2 && needGenerateMips && (textureFormat == vk::Format::eR8G8B8A8Srgb || textureFormat == vk::Format::eR8G8B8A8Unorm)) + { + generateMipmaps(*resources.textureImage, textureFormat, texWidth, texHeight, mipLevels); + } + + // 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 { - 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; - } +bool Renderer::createTextureImageView(TextureResources &resources) +{ + try + { + 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, 125}; // 50% alpha - 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 default 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; - } +bool Renderer::createSharedDefaultPBRTextures() +{ + try + { + unsigned char translucentPixel[4] = {128, 128, 128, 125}; // 50% alpha + 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 default 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 - 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( - *stagingBuffer, - *defaultTextureResources.textureImage, - width, - height, - regions - ); - - // 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; - } +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 + 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( + *stagingBuffer, + *defaultTextureResources.textureImage, + width, + height, + regions); + + // 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 (mipmapping disabled) - vk::SamplerCreateInfo samplerInfo{ - .magFilter = vk::Filter::eLinear, - .minFilter = vk::Filter::eLinear, - .mipmapMode = vk::SamplerMipmapMode::eNearest, // Disable mipmap filtering - .addressModeU = vk::SamplerAddressMode::eRepeat, - .addressModeV = vk::SamplerAddressMode::eRepeat, - .addressModeW = vk::SamplerAddressMode::eRepeat, - .mipLodBias = 0.0f, - .anisotropyEnable = VK_TRUE, - .maxAnisotropy = std::min(properties.limits.maxSamplerAnisotropy, 8.0f), - .compareEnable = VK_FALSE, - .compareOp = vk::CompareOp::eAlways, - .minLod = 0.0f, - .maxLod = 0.0f, // Force single mip level (no mipmapping) - .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; - } +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) - bool success = createTextureImage(resolvedId, tempResources); - - if (!success) { - std::cerr << "Failed to load texture: " << texturePath << std::endl; - } - - return success; +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.size() > 5 && resolvedId.rfind(".ktx2") == resolvedId.size() - 5) + { + 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; +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 - auto [textureImg, textureImgAllocation] = createImagePooled( - width, - height, - textureFormat, - vk::ImageTiling::eOptimal, - vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - resources.textureImage = std::move(textureImg); - resources.textureImageAllocation = std::move(textureImgAllocation); - - // GPU upload. Copy buffer to image in a single submit. - 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 = {static_cast(width), static_cast(height), 1} - }}; - uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, regions, 1); - - // Store the format for createTextureImageView - resources.format = textureFormat; - 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 - ); - - // 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; - } +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. + 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 = {static_cast(width), static_cast(height), 1}}}; + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, regions, 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()) { - 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 --- - auto [vertexBuffer, vertexBufferAllocation] = createBufferPooled( - vertexBufferSize, - vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, - vk::MemoryPropertyFlagBits::eDeviceLocal - ); - - auto [indexBuffer, indexBufferAllocation] = createBufferPooled( - indexBufferSize, - vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, - 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; - } +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()) + { + 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 { - // 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); - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; 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); - } - - // Create instance buffer for all entities (shaders always expect instance data) - auto* meshComponent = entity->GetComponent(); - if (meshComponent) { - std::vector instanceData; - - // CRITICAL FIX: Check if entity has any instance data first - 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; - } +bool Renderer::createUniformBuffers(Entity *entity) +{ + ensureThreadLocalVulkanInit(); + try + { + // 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); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; 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); + } + + // Create instance buffer for all entities (shaders always expect instance data) + auto *meshComponent = entity->GetComponent(); + if (meshComponent) + { + std::vector instanceData; + + // CRITICAL FIX: Check if entity has any instance data first + 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 1 light storage buffer per descriptor set - // Only PBR entities need storage buffers, so allocate for all entities using PBR - const uint32_t storageBufferDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities; - - std::array poolSizes = { +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 1 light storage buffer per descriptor set + // Only PBR entities need storage buffers, so allocate for all entities using PBR + // Storage buffers used per PBR descriptor set: + // - Binding 6: light storage buffer + // - Binding 7: Forward+ tile headers buffer + // - Binding 8: Forward+ tile indices buffer + const uint32_t storageBufferDescriptors = MAX_FRAMES_IN_FLIGHT * maxEntities * 3u; + + // 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 - }, + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = uboDescriptors}, vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eCombinedImageSampler, - .descriptorCount = textureDescriptors - }, + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = textureDescriptors + rqTexDescriptors}, vk::DescriptorPoolSize{ - .type = vk::DescriptorType::eStorageBuffer, - .descriptorCount = storageBufferDescriptors - } - }; - - // Create descriptor pool - vk::DescriptorPoolCreateInfo poolInfo{ - .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .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; - } + .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) { - // Resolve alias before taking the shared lock to avoid nested shared_lock on the same mutex - const std::string resolvedTexturePath = ResolveTextureId(texturePath); - std::shared_lock texLock(textureResourcesMutex); - try { - auto entityIt = entityResources.find(entity); - if (entityIt == entityResources.end()) return false; - - vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; - std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); - vk::DescriptorSetAllocateInfo allocInfo{ .descriptorPool = *descriptorPool, .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, .pSetLayouts = layouts.data() }; - - auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; - if (targetDescriptorSets.empty()) { - targetDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); - } - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - vk::DescriptorBufferInfo bufferInfo{ .buffer = *entityIt->second.uniformBuffers[i], .range = sizeof(UniformBufferObject) }; - - if (usePBR) { - // PBR sets now only have 7 bindings (0-6) - std::array descriptorWrites; - std::array imageInfos; - - descriptorWrites[0] = { .dstSet = *targetDescriptorSets[i], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo }; - - auto meshComponent = entity->GetComponent(); - std::vector pbrTexturePaths = { /* ... same as before ... */ }; - // ... (logic to get texture paths is the same) - { - 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]); - auto textureIt = textureResources.find(resolvedBindingPath); - TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - imageInfos[j] = { .sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal }; - descriptorWrites[j + 1] = { .dstSet = *targetDescriptorSets[i], .dstBinding = static_cast(j + 1), .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfos[j] }; - } - - vk::DescriptorBufferInfo lightBufferInfo{ .buffer = *lightStorageBuffers[i].buffer, .range = VK_WHOLE_SIZE }; - descriptorWrites[6] = { .dstSet = *targetDescriptorSets[i], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightBufferInfo }; - - device.updateDescriptorSets(descriptorWrites, {}); - } else { // Basic Pipeline - // ... (this part remains the same) - auto textureIt = textureResources.find(resolvedTexturePath); - TextureResources* texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - vk::DescriptorImageInfo imageInfo{ .sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .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 } - }; - 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; - } +bool Renderer::createDescriptorSets(Entity *entity, const std::string &texturePath, bool usePBR) +{ + // Resolve alias before taking the shared lock to avoid nested shared_lock on the same mutex + const std::string resolvedTexturePath = ResolveTextureId(texturePath); + std::shared_lock texLock(textureResourcesMutex); + try + { + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + return false; + + vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); + vk::DescriptorSetAllocateInfo allocInfo{.descriptorPool = *descriptorPool, .descriptorSetCount = MAX_FRAMES_IN_FLIGHT, .pSetLayouts = layouts.data()}; + + auto &targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.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)); + } + } + + // CRITICAL FIX: Validate that allocated descriptor sets have valid handles before use. + // The error "Invalid VkDescriptorSet Object 0x131a000000131a" indicates a corrupted + // or freed descriptor set handle. This can happen if: + // 1. Descriptor pool is exhausted and allocation fails silently + // 2. Descriptor set was destroyed elsewhere before this function + // 3. Memory corruption or race condition + // Checking validity prevents SIGSEGV crash when Vulkan tries to access invalid handles. + if (targetDescriptorSets.empty() || targetDescriptorSets.size() < MAX_FRAMES_IN_FLIGHT) + { + 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 at runtime to avoid + // updating descriptor sets that may be in use by pending command buffers. + // Other frames will be initialized at their own safe points. + size_t startIndex = static_cast(currentFrame); + size_t endIndex = startIndex + 1; + for (size_t i = startIndex; i < endIndex; i++) + { + // Validate descriptor set handle before dereferencing to prevent crash + // Check if the underlying VkDescriptorSet handle is valid (not null/default) + 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 = *entityIt->second.uniformBuffers[i], .range = sizeof(UniformBufferObject)}; + + if (usePBR) + { + // Build descriptor writes dynamically to avoid writing unused bindings + std::vector descriptorWrites; + std::array imageInfos; + // CRITICAL FIX: Buffer infos must remain in scope until updateDescriptorSets completes. + // Previously these were declared inside nested scopes, causing dangling pointers + // when descriptorWrites held pointers to them after they went out of scope. + 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 = {/* ... same as before ... */}; + // ... (logic to get texture paths is the same) + { + 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]); + auto textureIt = textureResources.find(resolvedBindingPath); + TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + imageInfos[j] = {.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .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].buffer, .range = VK_WHOLE_SIZE}; + descriptorWrites.push_back({.dstSet = *targetDescriptorSets[i], .dstBinding = 6, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eStorageBuffer, .pBufferInfo = &lightBufferInfo}); + + // CRITICAL FIX: PBR descriptor set layout ALWAYS requires bindings 7/8 (tileHeaders, tileLightIndices). + // We MUST bind valid buffers (real or dummy) to satisfy Vulkan validation. + // Creating dummy buffers here ensures bindings are never left uninitialized. + + // 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 < forwardPlusPerFrame.size()) + { + auto &f = forwardPlusPerFrame[i]; + if (f.tileHeaders == nullptr) + { + 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 < forwardPlusPerFrame.size()) + { + auto &f = forwardPlusPerFrame[i]; + if (f.tileLightIndices == nullptr) + { + 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}); + + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(descriptorWrites, {}); + } + } + else + { // Basic Pipeline + // ... (this part remains the same) + auto textureIt = textureResources.find(resolvedTexturePath); + TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + vk::DescriptorImageInfo imageInfo{.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .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; - } - - // 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; - } - - - // 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; - } +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.uboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.pbrImagesWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + it->second.basicImagesWritten.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) { - ensureThreadLocalVulkanInit(); - try { - // --- 1. For all entities, create mesh resources with deferred uploads --- - std::vector meshesNeedingUpload; - meshesNeedingUpload.reserve(entities.size()); - - for (Entity* entity : entities) { - if (!entity) { - continue; - } - - auto meshComponent = entity->GetComponent(); - if (!meshComponent) { - continue; - } - - if (!createMeshResources(meshComponent, 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. Batch all buffer copies into a single command buffer submission --- - if (!meshesNeedingUpload.empty()) { - 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]; - - vk::CommandBufferBeginInfo beginInfo{ - .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit - }; - commandBuffer.begin(beginInfo); - - for (MeshComponent* meshComponent : meshesNeedingUpload) { - auto it = meshResources.find(meshComponent); - if (it == meshResources.end()) { - continue; - } - MeshResources& res = it->second; - - if (res.vertexBufferSizeBytes > 0) { - vk::BufferCopy copyRegion{ - .srcOffset = 0, - .dstOffset = 0, - .size = res.vertexBufferSizeBytes - }; - commandBuffer.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, copyRegion); - } - - if (res.indexBufferSizeBytes > 0) { - vk::BufferCopy copyRegion{ - .srcOffset = 0, - .dstOffset = 0, - .size = res.indexBufferSizeBytes - }; - commandBuffer.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, copyRegion); - } - } - - commandBuffer.end(); - - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer - }; - - vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - { - std::lock_guard lock(queueMutex); - transferQueue.submit(submitInfo, *fence); - } - [[maybe_unused]] auto fenceResult = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - - // After upload, staging buffers can be released (RAII will destroy them) - for (MeshComponent* meshComponent : meshesNeedingUpload) { - auto it = meshResources.find(meshComponent); - if (it == meshResources.end()) { - continue; - } - MeshResources& res = it->second; - res.stagingVertexBuffer = nullptr; - res.stagingVertexBufferMemory = nullptr; - res.vertexBufferSizeBytes = 0; - res.stagingIndexBuffer = nullptr; - res.stagingIndexBufferMemory = nullptr; - res.indexBufferSizeBytes = 0; - } - } - - // --- 3. Create uniform buffers and descriptor sets per entity --- - for (Entity* entity : entities) { - if (!entity) { - continue; - } - - auto meshComponent = entity->GetComponent(); - if (!meshComponent) { - continue; - } - - 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; - } - } - - if (!createDescriptorSets(entity, texturePath, false)) { - std::cerr << "Failed to create basic descriptor sets for entity (batch): " - << entity->GetName() << std::endl; - return false; - } - - 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; - } +bool Renderer::preAllocateEntityResourcesBatch(const std::vector &entities) +{ + ensureThreadLocalVulkanInit(); + try + { + // --- 1. For all entities, create mesh resources with deferred uploads --- + std::vector meshesNeedingUpload; + meshesNeedingUpload.reserve(entities.size()); + + for (Entity *entity : entities) + { + if (!entity) + { + continue; + } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + { + continue; + } + + // Ensure local AABB is available for debug/probes + meshComponent->RecomputeLocalAABB(); + + if (!createMeshResources(meshComponent, 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()) + { + EnqueueMeshUploads(meshesNeedingUpload); + } + + // --- 3. Create uniform buffers and descriptor sets per entity --- + for (Entity *entity : entities) + { + if (!entity) + { + continue; + } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) + { + continue; + } + + 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; + } + } + + if (!createDescriptorSets(entity, texturePath, false)) + { + std::cerr << "Failed to create basic descriptor sets for entity (batch): " + << entity->GetName() << std::endl; + return false; + } + + 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); + } +} + +// Execute pending mesh uploads on the render thread after the per-frame fence wait +void Renderer::ProcessPendingMeshUploads() +{ + // Grab the list atomically + std::vector toProcess; + { + std::lock_guard lk(pendingMeshUploadsMutex); + if (pendingMeshUploads.empty()) + return; + toProcess.swap(pendingMeshUploads); + } + + // Filter to meshes that still have staged data + std::vector needsCopy; + needsCopy.reserve(toProcess.size()); + for (auto *meshComponent : toProcess) + { + 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()}; + 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 != nullptr && res.vertexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; + cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + } + if (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; + cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + } + } + + cb.end(); + + // Submit and wait on the GRAPHICS queue (single-threaded via queueMutex) + vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*cb}; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *fence); + } + (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + + // Clear staging once copies are complete + for (auto *meshComponent : needsCopy) + { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources &res = it->second; + res.stagingVertexBuffer = nullptr; + res.stagingVertexBufferMemory = nullptr; + res.vertexBufferSizeBytes = 0; + res.stagingIndexBuffer = nullptr; + res.stagingIndexBufferMemory = nullptr; + res.indexBufferSizeBytes = 0; + } + + // 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. + // This is safe at the render‑thread safe point and avoids partial TLAS builds. + asDevOverrideAllowRebuild = true; // allow rebuild even if frozen + 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; + } + + // Wait for GPU to finish using the old instance buffer before destroying it. + // External synchronization required (VVL): serialize against queue submits/present. + WaitIdle(); + + // Replace the old instance buffer with the new one + 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; - } + 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; - } + 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() - }; - - transparentDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); - - // Update each descriptor set to point to our single off-screen opaque color image - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - vk::DescriptorImageInfo imageInfo{ - .sampler = *opaqueSceneColorSampler, - .imageView = *opaqueSceneColorImageView, - .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 - }; - - device.updateDescriptorSets(descriptorWrite, nullptr); - } +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 our single off-screen opaque color image + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + vk::DescriptorImageInfo imageInfo{ + .sampler = *opaqueSceneColorSampler, + .imageView = *opaqueSceneColorImageView, + .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() - }; - - 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 - }; - - 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 { - // Create the image - 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, // <-- Note the new usage flags - vk::MemoryPropertyFlagBits::eDeviceLocal); - - opaqueSceneColorImage = std::move(image); - // We don't need a member for the allocation, it's managed by the unique_ptr - - // Create the image view - opaqueSceneColorImageView = createImageView(opaqueSceneColorImage, swapChainImageFormat, vk::ImageAspectFlagBits::eColor); - - // Create the sampler - 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; - } +bool Renderer::createOpaqueSceneColorResources() +{ + try + { + // Create the image + 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, // <-- Note the new usage flags + vk::MemoryPropertyFlagBits::eDeviceLocal); + + opaqueSceneColorImage = std::move(image); + // We don't need a member for the allocation, it's managed by the unique_ptr + + // Create the image view + opaqueSceneColorImageView = createImageView(opaqueSceneColorImage, swapChainImageFormat, vk::ImageAspectFlagBits::eColor); + + // Create the sampler + 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); - } - [[maybe_unused]] auto fenceResult2 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - } catch (const std::exception& e) { - std::cerr << "Failed to copy buffer: " << e.what() << std::endl; - throw; - } +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); + } + [[maybe_unused]] auto fenceResult2 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } + 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) { - try { - // Create image - vk::ImageCreateInfo imageInfo{ - .imageType = vk::ImageType::e2D, - .format = format, - .extent = {width, height, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .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; - } + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties) +{ + try + { + // Create image + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .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) { - 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); - - 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; - } + 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) { - try { - ensureThreadLocalVulkanInit(); - // Create image view - vk::ImageViewCreateInfo viewInfo{ - .image = *image, - .viewType = vk::ImageViewType::e2D, - .format = format, - .subresourceRange = { - .aspectMask = aspectFlags, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = 1 - } - }; - - return { device, viewInfo }; - } catch (const std::exception& e) { - std::cerr << "Failed to create image view: " << e.what() << std::endl; - throw; - } +vk::raii::ImageView Renderer::createImageView(vk::raii::Image &image, vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels) +{ + try + { + ensureThreadLocalVulkanInit(); + // Create image view + vk::ImageViewCreateInfo viewInfo{ + .image = *image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1}}; + + 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) { - 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 - vk::ImageMemoryBarrier barrier{ - .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 = 1 - } - }; - - // Set access masks and pipeline stages based on layouts - vk::PipelineStageFlags sourceStage; - vk::PipelineStageFlags destinationStage; - - if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eNone; - barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - - sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; - destinationStage = vk::PipelineStageFlagBits::eTransfer; - } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - - sourceStage = vk::PipelineStageFlagBits::eTransfer; - destinationStage = vk::PipelineStageFlagBits::eFragmentShader; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eNone; - barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite; - - sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; - destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; - } else if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eDepthStencilReadOnlyOptimal) { - // Support for shadow map creation: transition from undefined to read-only depth layout - barrier.srcAccessMask = vk::AccessFlagBits::eNone; - barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead; - - sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; - destinationStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; - } else { - throw std::invalid_argument("Unsupported layout transition!"); - } - - // Add a barrier to command buffer - commandBuffer.pipelineBarrier( - sourceStage, destinationStage, - vk::DependencyFlagBits::eByRegion, - nullptr, - nullptr, - barrier - ); - std::cout << "[transitionImageLayout] recorded barrier image=" << (void*)image << " old=" << static_cast(oldLayout) << " new=" << static_cast(newLayout) << std::endl; - - // End command buffer - commandBuffer.end(); - - // Submit command buffer - - // Submit transition; protect submit with mutex but wait outside - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer - }; - vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - { - std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, *fence); - } - [[maybe_unused]] auto fenceResult3 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - } catch (const std::exception& e) { - std::cerr << "Failed to transition image layout: " << e.what() << std::endl; - throw; - } +void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, uint32_t mipLevels) +{ + 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 = 1}}; + + // 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(); + + // CRITICAL FIX: Signal timeline semaphore after layout transition to ensure render loop + // waits for ALL texture operations (initialization + streaming) before sampling. + // Without this, textures transitioned via this function (e.g., default textures during init) + // can be sampled before their layout transitions complete, causing validation errors: + // "expects SHADER_READ_ONLY_OPTIMAL--instead, current layout is UNDEFINED" + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = uploadsTimeline != nullptr; + uint64_t signalValue = 0; + { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{}; + if (canSignalTimeline) + { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue}; + submitInfo.pNext = &timelineInfo; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &*uploadsTimeline; + } + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &*commandBuffer; + graphicsQueue.submit(submitInfo, *fence); + } + [[maybe_unused]] auto fenceResult3 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } + 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, const std::vector& regions) const { - 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(); - - // Submit command buffer - vk::SubmitInfo submitInfo{ - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffer - }; - - // Protect submit with queue mutex, wait outside - vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - { - std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, *fence); - } - [[maybe_unused]] auto fenceResult4 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - } catch (const std::exception& e) { - std::cerr << "Failed to copy buffer to image: " << e.what() << std::endl; - throw; - } +void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t width, uint32_t height, const std::vector ®ions) +{ + 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(); + + // CRITICAL FIX: Signal timeline semaphore after buffer copy to ensure render loop + // waits for ALL texture uploads (initialization + streaming) before sampling. + // Without this, default textures copied via this function during initialization can be + // sampled before the copy completes, causing validation errors and flickering. + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = uploadsTimeline != nullptr; + uint64_t signalValue = 0; + { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submitInfo{}; + if (canSignalTimeline) + { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue}; + submitInfo.pNext = &timelineInfo; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &*uploadsTimeline; + } + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &*commandBuffer; + graphicsQueue.submit(submitInfo, *fence); + } + [[maybe_unused]] auto fenceResult4 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } + 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 - // This ensures no GPU operations are using the old buffers - device.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 = 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(); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create or resize light storage buffers: " << e.what() << std::endl; - return false; - } +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 = 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 == nullptr) + continue; + if (lightStorageBuffers[i].buffer == nullptr) + 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() { - try { - // Iterate through all entity resources and update their PBR descriptor sets - for (auto& resources : entityResources | std::views::values) { - // Only update PBR descriptor sets (they have light buffer bindings) - if (!resources.pbrDescriptorSets.empty()) { - for (size_t i = 0; i < resources.pbrDescriptorSets.size() && i < lightStorageBuffers.size(); ++i) { - 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 - device.updateDescriptorSets(descriptorWrite, {}); - } - } - } - } - } catch (const std::exception& e) { - std::cerr << "Failed to update descriptor sets with new light buffers: " << e.what() << std::endl; - } +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; + } } -// Update the light storage buffer with current light data -bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vector& lights) { - 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); - - // Calculate light space matrix for shadow mapping - glm::mat4 lightProjection, lightView; - if (light.type == ExtractedLight::Type::Directional) { - float orthoSize = 50.0f; - lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, 0.1f, 100.0f); - lightView = glm::lookAt(light.position, light.position + light.direction, glm::vec3(0.0f, 1.0f, 0.0f)); - } 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; - } +// 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 == nullptr)) + headersBuf = *f.tileHeaders; + if (!(f.tileLightIndices == nullptr)) + indicesBuf = *f.tileLightIndices; + } + + // Resolve current frame lights buffer + vk::Buffer lightsBuf{}; + if (frameIndex < lightStorageBuffers.size() && !(lightStorageBuffers[frameIndex].buffer == nullptr)) + { + lightsBuf = *lightStorageBuffers[frameIndex].buffer; + } + + // CRITICAL FIX: PBR descriptor set layout ALWAYS declares bindings 6/7/8 (see renderer_pipelines.cpp lines 112-135), + // so these bindings MUST be valid even when Forward+ is disabled. If real Forward+ buffers don't exist, + // we must ensure minimal dummy buffers are bound to satisfy the descriptor set layout requirements. + // Without valid bindings, Vulkan validation reports: "descriptor [binding X] is being used in draw but has never been updated" + + // 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 == nullptr) + { + 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 == nullptr)) + { + 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 == nullptr) + { + 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 == nullptr)) + 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 == nullptr) + { + 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 == nullptr)) + indicesBuf = *f.tileLightIndices; + } + } + + std::vector writes; + vk::DescriptorBufferInfo lightsInfo{}; + vk::DescriptorBufferInfo headersInfo{}; + vk::DescriptorBufferInfo indicesInfo{}; + 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 == nullptr)) + { + 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}; + + for (auto &kv : entityResources) + { + auto &res = kv.second; + if (res.pbrDescriptorSets.empty() || frameIndex >= res.pbrDescriptorSets.size()) + continue; + + // CRITICAL: Validate descriptor set handle before using it + // 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}); + } + + 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) +{ + 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); + + // Calculate light space matrix for shadow mapping + glm::mat4 lightProjection, lightView; + if (light.type == ExtractedLight::Type::Directional) + { + float orthoSize = 50.0f; + lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, 0.1f, 100.0f); + lightView = glm::lookAt(light.position, light.position + light.direction, glm::vec3(0.0f, 1.0f, 0.0f)); + } + 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)); - } - 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::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; - job.data = std::move(data); - job.width = width; - job.height = height; - job.channels = channels; - { - std::lock_guard lk(pendingTextureJobsMutex); - pendingTextureJobs.emplace_back(std::move(job)); - } - 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)); +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; + job.data = std::move(data); + job.width = width; + job.height = height; + job.channels = channels; + { + 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)); - } +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)); + } } -void Renderer::RegisterTextureUser(const std::string& textureId, Entity* entity) { - if (textureId.empty() || !entity) return; +// 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); + } + } + + 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 != nullptr; + { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + if (canSignal) + { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{.signalSemaphoreValueCount = 1, .pSignalSemaphoreValues = &signalValue}; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + transferQueue.submit(submit, *fence); + } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + + // 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; + } + } + } + }); + } +} - // 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; - } +void Renderer::StopUploadsWorker() +{ + stopUploadsWorker.store(true, std::memory_order_relaxed); + pendingTextureCv.notify_all(); + for (auto &th : uploadsWorkerThreads) + { + if (th.joinable()) + th.join(); + } + uploadsWorkerThreads.clear(); +} - std::lock_guard lk(textureUsersMutex); - textureToEntities[canonicalId].push_back(entity); +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; - } - - for (Entity* entity : users) { - if (!entity) continue; - auto meshComponent = entity->GetComponent(); - if (!meshComponent) continue; - - // Choose a primary texture path hint for basic pipeline; PBR - // descriptor creation will pull all PBR texture paths directly - // from the mesh component. - std::string basicTexPath = meshComponent->GetTexturePath(); - if (basicTexPath.empty()) { - basicTexPath = meshComponent->GetBaseColorTexturePath(); - } - - // Recreate/refresh descriptor sets for this entity so they now - // bind the just-uploaded texture instead of the default. - createDescriptorSets(entity, basicTexPath, false); - createDescriptorSets(entity, basicTexPath, true); - } +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); + } } -void Renderer::ProcessPendingTextureJobs(uint32_t maxJobs, - bool includeCritical, - bool includeNonCritical) { - // 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; - for (auto& job : jobs) { - 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())); - } +void Renderer::MarkEntityDescriptorsDirty(Entity *entity) +{ + if (!entity) + return; + std::lock_guard lk(dirtyEntitiesMutex); + descriptorDirtyEntities.insert(entity); +} + +bool Renderer::updateDescriptorSetsForFrame(Entity *entity, + 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; + } + std::shared_lock texLock(textureResourcesMutex); + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) + return false; + + // Ensure we have a valid UBO for this frame before attempting descriptor writes + if (frameIndex >= entityIt->second.uniformBuffers.size() || + frameIndex >= entityIt->second.uniformBuffersMapped.size() || + *entityIt->second.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 ? entityIt->second.pbrDescriptorSets : entityIt->second.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 = *entityIt->second.uniformBuffers[frameIndex], .range = sizeof(UniformBufferObject)}; + + // Ensure uboBindingWritten vector is sized + if (entityIt->second.uboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + entityIt->second.uboBindingWritten.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; + // 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 (!entityIt->second.uboBindingWritten[frameIndex]) + { + writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}); + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + entityIt->second.uboBindingWritten[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 auto resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); + auto textureIt = textureResources.find(resolvedBindingPath); + TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + imageInfos[j] = {.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .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}); + } + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, {}); + } + // CRITICAL FIX: Only mark UBO as written if we actually wrote it (not during imagesOnly updates) + if (!imagesOnly) + { + entityIt->second.uboBindingWritten[frameIndex] = true; + } + } + else + { + const std::string resolvedTexturePath = ResolveTextureId(texturePath); + auto textureIt = textureResources.find(resolvedTexturePath); + TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; + vk::DescriptorImageInfo imageInfo{.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .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) + { + 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, {}); + } + entityIt->second.uboBindingWritten[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, {}); + } + entityIt->second.uboBindingWritten[frameIndex] = true; + } + } + return true; } +void Renderer::ProcessDirtyDescriptorsForFrame(uint32_t frameIndex) +{ + std::vector dirty; + { + std::lock_guard lk(dirtyEntitiesMutex); + if (descriptorDirtyEntities.empty()) + return; + dirty.reserve(descriptorDirtyEntities.size()); + for (auto *e : descriptorDirtyEntities) + dirty.push_back(e); + descriptorDirtyEntities.clear(); + } + + for (Entity *entity : dirty) + { + if (!entity) + continue; + 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. + updateDescriptorSetsForFrame(entity, basicTexPath, false, frameIndex, /*imagesOnly=*/true); + updateDescriptorSetsForFrame(entity, basicTexPath, true, frameIndex, /*imagesOnly=*/true); + // Do not touch descriptors for other frames while their command buffers may be pending. + } +} + +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; + for (auto &job : jobs) + { + 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, - const std::vector& regions, - uint32_t mipLevels) { - ensureThreadLocalVulkanInit(); - try { - // Use a temporary transient command pool for the GRAPHICS queue family 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 cbs(device, allocInfo); - vk::raii::CommandBuffer& cb = cbs[0]; - - vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; - cb.begin(beginInfo); - - // Barrier: Undefined -> TransferDstOptimal - vk::ImageMemoryBarrier toTransfer{ - .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 - } - }; - toTransfer.srcAccessMask = vk::AccessFlagBits::eNone; - toTransfer.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - cb.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eTransfer, - vk::DependencyFlagBits::eByRegion, - nullptr, nullptr, toTransfer); - - // Copy - cb.copyBufferToImage(staging, image, vk::ImageLayout::eTransferDstOptimal, regions); - - // Barrier: TransferDstOptimal -> ShaderReadOnlyOptimal - vk::ImageMemoryBarrier toShader{ - .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 - } - }; - toShader.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - // Keep dstAccessMask empty; visibility is ensured via submission ordering and timeline wait - cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, - vk::PipelineStageFlagBits::eTransfer, - vk::DependencyFlagBits::eByRegion, - nullptr, nullptr, toShader); - - cb.end(); - - // Submit once on the GRAPHICS queue; signal uploads timeline if available - vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - bool canSignalTimeline = uploadsTimeline != nullptr; - uint64_t signalValue = 0; - { - std::lock_guard lock(queueMutex); - vk::SubmitInfo submit{}; - if (canSignalTimeline) { - signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue - }; - submit.pNext = &timelineInfo; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &*uploadsTimeline; - } - submit.commandBufferCount = 1; - submit.pCommandBuffers = &*cb; - - graphicsQueue.submit(submit, *fence); - } - [[maybe_unused]] auto fenceResult5 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - } catch (const std::exception& e) { - std::cerr << "uploadImageFromStaging failed: " << e.what() << std::endl; - throw; - } +void Renderer::uploadImageFromStaging(vk::Buffer staging, + vk::Image image, + vk::Format format, + const std::vector ®ions, + 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 != nullptr; + uint64_t signalValue = 0; + { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + if (canSignalTimeline) + { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue}; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + + transferQueue.submit(submit, *fence); + } + [[maybe_unused]] auto fenceResult5 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + + // 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(); + + // CRITICAL FIX: Signal timeline semaphore after mipmap generation to ensure render loop + // waits for BOTH upload (transfer queue) AND mipmap generation (graphics queue) to complete. + // Without this, textures can be sampled before mipmaps are fully generated, causing + // validation errors: "expects SHADER_READ_ONLY_OPTIMAL--instead, current layout is UNDEFINED" + // This happens because uploadImageFromStaging signals timeline N, but generateMipmaps runs + // on graphics queue without updating timeline, creating a race where rendering waits for N + // (upload complete) but not for mipmap generation (which may still be running). + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + bool canSignalTimeline = uploadsTimeline != nullptr; + uint64_t signalValue = 0; + { + std::lock_guard lock(queueMutex); + vk::SubmitInfo submit{}; + if (canSignalTimeline) + { + signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue}; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; + } + submit.commandBufferCount = 1; + submit.pCommandBuffers = &*cb; + graphicsQueue.submit(submit, *fence); + } + (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); } diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp index 626a86f6..bd98327a 100644 --- a/attachments/simple_engine/renderer_utils.cpp +++ b/attachments/simple_engine/renderer_utils.cpp @@ -1,288 +1,343 @@ +/* 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 // 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; - } +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; - } +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; - } +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; +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; - } +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; - } +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; - } - // Check for present support - if (!indices.presentFamily.has_value() && 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; +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; + } + // Check for present support + if (!indices.presentFamily.has_value() && 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; +SwapChainSupportDetails Renderer::querySwapChainSupport(const vk::raii::PhysicalDevice &device) +{ + SwapChainSupportDetails details; - // Get surface capabilities - details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); - // Get surface formats - details.formats = device.getSurfaceFormatsKHR(surface); + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); - // Get present modes - details.presentModes = device.getSurfacePresentModesKHR(surface); + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); - return details; + return details; } // Check device extension support -bool Renderer::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { - auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties(); - - // Print available extensions for debugging - std::cout << "Available extensions:" << std::endl; - for (const auto& extension : availableDeviceExtensions) { - std::cout << " " << extension.extensionName << std::endl; - } - - // 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; - } - - // Check which optional extensions are supported - std::set optionalExtensionsSet(optionalDeviceExtensions.begin(), optionalDeviceExtensions.end()); - std::cout << "Supported optional extensions:" << std::endl; - for (const auto& extension : availableDeviceExtensions) { - if (optionalExtensionsSet.contains(extension.extensionName)) { - std::cout << " " << extension.extensionName << " (supported)" << std::endl; - } - } - - return true; +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; +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]; +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; +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; - } +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() { - device.waitIdle(); +void Renderer::WaitIdle() +{ + // 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(); } - diff --git a/attachments/simple_engine/resource_manager.cpp b/attachments/simple_engine/resource_manager.cpp index 5102539c..006c16b4 100644 --- a/attachments/simple_engine/resource_manager.cpp +++ b/attachments/simple_engine/resource_manager.cpp @@ -1,3 +1,19 @@ +/* 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 "resource_manager.h" #include @@ -8,21 +24,28 @@ // This implementation corresponds to the Engine_Architecture chapter in the tutorial: // @see en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc -bool Resource::Load() { - loaded = true; - return true; +bool Resource::Load() +{ + loaded = true; + return true; } -void Resource::Unload() { - loaded = false; +void Resource::Unload() +{ + loaded = false; } -void ResourceManager::UnloadAllResources() { - for (auto& val : resources | std::views::values) { - for (auto& loadedResource : val | std::views::values) { - loadedResource->Unload(); - } - val.clear(); - } - resources.clear(); +void ResourceManager::UnloadAllResources() +{ + for (auto &kv : resources) + { + auto &val = kv.second; + for (auto &innerKv : val) + { + auto &loadedResource = innerKv.second; + loadedResource->Unload(); + } + val.clear(); + } + resources.clear(); } diff --git a/attachments/simple_engine/resource_manager.h b/attachments/simple_engine/resource_manager.h index cfda78f1..2e0b4c53 100644 --- a/attachments/simple_engine/resource_manager.h +++ b/attachments/simple_engine/resource_manager.h @@ -1,115 +1,154 @@ +/* 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 /** * @brief Base class for all resources. */ -class Resource final { -protected: - std::string resourceId; - bool loaded = false; - -public: - /** - * @brief Constructor with a resource ID. - * @param id The unique identifier for the resource. - */ - explicit Resource(const std::string& id) : resourceId(id) {} - - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~Resource() = default; - - /** - * @brief Get the resource ID. - * @return The resource ID. - */ - const std::string& GetId() const { return resourceId; } - - /** - * @brief Check if the resource is loaded. - * @return True if the resource is loaded, false otherwise. - */ - bool IsLoaded() const { return loaded; } - - /** - * @brief Load the resource. - * @return True if the resource was loaded successfully, false otherwise. - */ - virtual bool Load(); - - /** - * @brief Unload the resource. - */ - virtual void Unload(); +class Resource final +{ + protected: + std::string resourceId; + bool loaded = false; + + public: + /** + * @brief Constructor with a resource ID. + * @param id The unique identifier for the resource. + */ + explicit Resource(const std::string &id) : + resourceId(id) + {} + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~Resource() = default; + + /** + * @brief Get the resource ID. + * @return The resource ID. + */ + const std::string &GetId() const + { + return resourceId; + } + + /** + * @brief Check if the resource is loaded. + * @return True if the resource is loaded, false otherwise. + */ + bool IsLoaded() const + { + return loaded; + } + + /** + * @brief Load the resource. + * @return True if the resource was loaded successfully, false otherwise. + */ + virtual bool Load(); + + /** + * @brief Unload the resource. + */ + virtual void Unload(); }; /** * @brief Template class for resource handles. * @tparam T The type of resource. */ -template -class ResourceHandle { -private: - std::string resourceId; - class ResourceManager* resourceManager = nullptr; - -public: - /** - * @brief Default constructor. - */ - ResourceHandle() = default; - - /** - * @brief Constructor with a resource ID and resource manager. - * @param id The resource ID. - * @param manager The resource manager. - */ - ResourceHandle(const std::string& id, class ResourceManager* manager) - : resourceId(id), resourceManager(manager) {} - - /** - * @brief Get the resource. - * @return A pointer to the resource, or nullptr if not found. - */ - T* Get() const; - - /** - * @brief Check if the handle is valid. - * @return True if the handle is valid, false otherwise. - */ - bool IsValid() const; - - /** - * @brief Get the resource ID. - * @return The resource ID. - */ - const std::string& GetId() const { return resourceId; } - - /** - * @brief Convenience operator for accessing the resource. - * @return A pointer to the resource. - */ - T* operator->() const { return Get(); } - - /** - * @brief Convenience operator for dereferencing the resource. - * @return A reference to the resource. - */ - T& operator*() const { return *Get(); } - - /** - * @brief Convenience operator for checking if the handle is valid. - * @return True if the handle is valid, false otherwise. - */ - operator bool() const { return IsValid(); } +template +class ResourceHandle +{ + private: + std::string resourceId; + class ResourceManager *resourceManager = nullptr; + + public: + /** + * @brief Default constructor. + */ + ResourceHandle() = default; + + /** + * @brief Constructor with a resource ID and resource manager. + * @param id The resource ID. + * @param manager The resource manager. + */ + ResourceHandle(const std::string &id, class ResourceManager *manager) : + resourceId(id), resourceManager(manager) + {} + + /** + * @brief Get the resource. + * @return A pointer to the resource, or nullptr if not found. + */ + T *Get() const; + + /** + * @brief Check if the handle is valid. + * @return True if the handle is valid, false otherwise. + */ + bool IsValid() const; + + /** + * @brief Get the resource ID. + * @return The resource ID. + */ + const std::string &GetId() const + { + return resourceId; + } + + /** + * @brief Convenience operator for accessing the resource. + * @return A pointer to the resource. + */ + T *operator->() const + { + return Get(); + } + + /** + * @brief Convenience operator for dereferencing the resource. + * @return A reference to the resource. + */ + T &operator*() const + { + return *Get(); + } + + /** + * @brief Convenience operator for checking if the handle is valid. + * @return True if the handle is valid, false otherwise. + */ + operator bool() const + { + return IsValid(); + } }; /** @@ -118,135 +157,151 @@ class ResourceHandle { * This class implements the resource management system as described in the Engine_Architecture chapter: * @see en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc */ -class ResourceManager final { -private: - std::unordered_map>> resources; - -public: - /** - * @brief Default constructor. - */ - ResourceManager() = default; - - /** - * @brief Virtual destructor for proper cleanup. - */ - virtual ~ResourceManager() = default; - - /** - * @brief Load a resource. - * @tparam T The type of resource. - * @tparam Args The types of arguments to pass to the resource constructor. - * @param id The resource ID. - * @param args The arguments to pass to the resource constructor. - * @return A handle to the resource. - */ - template - ResourceHandle LoadResource(const std::string& id, Args&&... args) { - static_assert(std::is_base_of::value, "T must derive from Resource"); - - // Check if the resource already exists - auto& typeResources = resources[std::type_index(typeid(T))]; - auto it = typeResources.find(id); - if (it != typeResources.end()) { - return ResourceHandle(id, this); - } - - // Create and load the resource - auto resource = std::make_unique(id, std::forward(args)...); - if (!resource->Load()) { - throw std::runtime_error("Failed to load resource: " + id); - } - - // Store the resource - typeResources[id] = std::move(resource); - return ResourceHandle(id, this); - } - - /** - * @brief Get a resource. - * @tparam T The type of resource. - * @param id The resource ID. - * @return A pointer to the resource, or nullptr if not found. - */ - template - T* GetResource(const std::string& id) { - static_assert(std::is_base_of::value, "T must derive from Resource"); - - auto typeIt = resources.find(std::type_index(typeid(T))); - if (typeIt == resources.end()) { - return nullptr; - } - - auto& typeResources = typeIt->second; - auto resourceIt = typeResources.find(id); - if (resourceIt == typeResources.end()) { - return nullptr; - } - - return static_cast(resourceIt->second.get()); - } - - /** - * @brief Check if a resource exists. - * @tparam T The type of resource. - * @param id The resource ID. - * @return True if the resource exists, false otherwise. - */ - template - bool HasResource(const std::string& id) { - static_assert(std::is_base_of::value, "T must derive from Resource"); - - auto typeIt = resources.find(std::type_index(typeid(T))); - if (typeIt == resources.end()) { - return false; - } - - auto& typeResources = typeIt->second; - return typeResources.contains(id); - } - - /** - * @brief Unload a resource. - * @tparam T The type of resource. - * @param id The resource ID. - * @return True if the resource was unloaded, false otherwise. - */ - template - bool UnloadResource(const std::string& id) { - static_assert(std::is_base_of::value, "T must derive from Resource"); - - auto typeIt = resources.find(std::type_index(typeid(T))); - if (typeIt == resources.end()) { - return false; - } - - auto& typeResources = typeIt->second; - auto resourceIt = typeResources.find(id); - if (resourceIt == typeResources.end()) { - return false; - } - - resourceIt->second->Unload(); - typeResources.erase(resourceIt); - return true; - } - - /** - * @brief Unload all resources. - */ - void UnloadAllResources(); +class ResourceManager final +{ + private: + std::unordered_map>> resources; + + public: + /** + * @brief Default constructor. + */ + ResourceManager() = default; + + /** + * @brief Virtual destructor for proper cleanup. + */ + virtual ~ResourceManager() = default; + + /** + * @brief Load a resource. + * @tparam T The type of resource. + * @tparam Args The types of arguments to pass to the resource constructor. + * @param id The resource ID. + * @param args The arguments to pass to the resource constructor. + * @return A handle to the resource. + */ + template + ResourceHandle LoadResource(const std::string &id, Args &&...args) + { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + // Check if the resource already exists + auto &typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(id); + if (it != typeResources.end()) + { + return ResourceHandle(id, this); + } + + // Create and load the resource + auto resource = std::make_unique(id, std::forward(args)...); + if (!resource->Load()) + { + throw std::runtime_error("Failed to load resource: " + id); + } + + // Store the resource + typeResources[id] = std::move(resource); + return ResourceHandle(id, this); + } + + /** + * @brief Get a resource. + * @tparam T The type of resource. + * @param id The resource ID. + * @return A pointer to the resource, or nullptr if not found. + */ + template + T *GetResource(const std::string &id) + { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) + { + return nullptr; + } + + auto &typeResources = typeIt->second; + auto resourceIt = typeResources.find(id); + if (resourceIt == typeResources.end()) + { + return nullptr; + } + + return static_cast(resourceIt->second.get()); + } + + /** + * @brief Check if a resource exists. + * @tparam T The type of resource. + * @param id The resource ID. + * @return True if the resource exists, false otherwise. + */ + template + bool HasResource(const std::string &id) + { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) + { + return false; + } + + auto &typeResources = typeIt->second; + return typeResources.contains(id); + } + + /** + * @brief Unload a resource. + * @tparam T The type of resource. + * @param id The resource ID. + * @return True if the resource was unloaded, false otherwise. + */ + template + bool UnloadResource(const std::string &id) + { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + auto typeIt = resources.find(std::type_index(typeid(T))); + if (typeIt == resources.end()) + { + return false; + } + + auto &typeResources = typeIt->second; + auto resourceIt = typeResources.find(id); + if (resourceIt == typeResources.end()) + { + return false; + } + + resourceIt->second->Unload(); + typeResources.erase(resourceIt); + return true; + } + + /** + * @brief Unload all resources. + */ + void UnloadAllResources(); }; // Implementation of ResourceHandle methods -template -T* ResourceHandle::Get() const { - if (!resourceManager) return nullptr; - return resourceManager->GetResource(resourceId); +template +T *ResourceHandle::Get() const +{ + if (!resourceManager) + return nullptr; + return resourceManager->GetResource(resourceId); } -template -bool ResourceHandle::IsValid() const { - if (!resourceManager) return false; - return resourceManager->HasResource(resourceId); +template +bool ResourceHandle::IsValid() const +{ + if (!resourceManager) + return false; + return resourceManager->HasResource(resourceId); } diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 6ed0698c..9c28dc8a 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -1,31 +1,51 @@ +/* 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 "scene_loading.h" +#include "animation_component.h" +#include "camera_component.h" #include "engine.h" -#include "transform_component.h" #include "mesh_component.h" -#include "camera_component.h" -#include +#include "transform_component.h" #include #include +#include /** * @brief Calculate bounding box dimensions for a MaterialMesh. * @param materialMesh The MaterialMesh to analyze. * @return The size of the bounding box (max - min for each axis). */ -glm::vec3 CalculateBoundingBoxSize(const MaterialMesh& materialMesh) { - if (materialMesh.vertices.empty()) { - return glm::vec3(0.0f); - } - - glm::vec3 minBounds = materialMesh.vertices[0].position; - glm::vec3 maxBounds = materialMesh.vertices[0].position; - - for (const auto& vertex : materialMesh.vertices) { - minBounds = glm::min(minBounds, vertex.position); - maxBounds = glm::max(maxBounds, vertex.position); - } - - return maxBounds - minBounds; +glm::vec3 CalculateBoundingBoxSize(const MaterialMesh &materialMesh) +{ + if (materialMesh.vertices.empty()) + { + return glm::vec3(0.0f); + } + + glm::vec3 minBounds = materialMesh.vertices[0].position; + glm::vec3 maxBounds = materialMesh.vertices[0].position; + + for (const auto &vertex : materialMesh.vertices) + { + minBounds = glm::min(minBounds, vertex.position); + maxBounds = glm::max(maxBounds, vertex.position); + } + + return maxBounds - minBounds; } /** @@ -37,298 +57,567 @@ glm::vec3 CalculateBoundingBoxSize(const MaterialMesh& materialMesh) { * @param rotation The rotation to apply to the model (default: no rotation). * @param scale The scale to apply to the model (default: unit scale). */ -bool LoadGLTFModel(Engine* engine, const std::string& modelPath, - const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale) { - // Get the model loader and renderer - ModelLoader* modelLoader = engine->GetModelLoader(); - Renderer* renderer = engine->GetRenderer(); - - if (!modelLoader || !renderer) { - std::cerr << "Error: ModelLoader or Renderer is null" << std::endl; - if (renderer) { renderer->SetLoading(false); } - return false; - } - // Ensure loading flag is cleared on any exit from this function - struct LoadingGuard { Renderer* r; ~LoadingGuard(){ r->SetLoading(false); } } loadingGuard{renderer}; - - // Extract model name from file path for entity naming - std::filesystem::path modelFilePath(modelPath); - std::string modelName = modelFilePath.stem().string(); // Get filename without extension - - try { - // Load the complete GLTF model with all textures and lighting on the main thread - Model* loadedModel = modelLoader->LoadGLTF(modelPath); - if (!loadedModel) { - std::cerr << "Failed to load GLTF model: " << modelPath << std::endl; - return false; - } - - std::cout << "Successfully loaded GLTF model with all textures and lighting: " << modelPath << std::endl; - - // Extract lights from the model and transform them to world space - std::vector extractedLights = modelLoader->GetExtractedLights(modelPath); - - // Create a transformation matrix from position, rotation, and scale - glm::mat4 transformMatrix = glm::mat4(1.0f); - transformMatrix = glm::translate(transformMatrix, position); - transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); - transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); - transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); - transformMatrix = glm::scale(transformMatrix, scale); - - // Transform all light positions from local model space to world space - // Also transform the light direction (for directional lights) - glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); - for (auto& light : extractedLights) { - glm::vec4 worldPos = transformMatrix * glm::vec4(light.position, 1.0f); - light.position = glm::vec3(worldPos); - light.direction = glm::normalize(normalMatrix * light.direction); - } - - renderer->SetStaticLights(extractedLights); - - // Extract and apply cameras from the GLTF model - const std::vector& cameras = loadedModel->GetCameras(); - if (!cameras.empty()) { - const CameraData& gltfCamera = cameras[0]; // Use the first camera - - // Find or create a camera entity to replace the default one - Entity* cameraEntity = engine->GetEntity("Camera"); - if (!cameraEntity) { - // Create a new camera entity if none exists - cameraEntity = engine->CreateEntity("Camera"); - if (cameraEntity) { - cameraEntity->AddComponent(); - cameraEntity->AddComponent(); - } - } - - if (cameraEntity) { - // Update the camera transform with GLTF data - auto* cameraTransform = cameraEntity->GetComponent(); - if (cameraTransform) { - // Apply the transformation matrix to the camera position - glm::vec4 worldPos = transformMatrix * glm::vec4(gltfCamera.position, 1.0f); - cameraTransform->SetPosition(glm::vec3(worldPos)); - - // Apply rotation from GLTF camera - glm::vec3 eulerAngles = glm::eulerAngles(gltfCamera.rotation); - cameraTransform->SetRotation(eulerAngles); - } - - // Update the camera component with GLTF properties - auto* camera = cameraEntity->GetComponent(); - if (camera) { - camera->ForceViewMatrixUpdate(); // Only sets viewMatrixDirty flag, doesn't change camera orientation - if (gltfCamera.isPerspective) { - camera->SetFieldOfView(glm::degrees(gltfCamera.fov)); // Convert radians to degrees - camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); - if (gltfCamera.aspectRatio > 0.0f) { - camera->SetAspectRatio(gltfCamera.aspectRatio); - } - } else { - // Handle orthographic camera if needed - camera->SetProjectionType(CameraComponent::ProjectionType::Orthographic); - camera->SetOrthographicSize(gltfCamera.orthographicSize, gltfCamera.orthographicSize); - camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); - } - - // Set this as the active camera - engine->SetActiveCamera(camera); - } - } - } - - // Get the material meshes from the loaded model - const std::vector& materialMeshes = modelLoader->GetMaterialMeshes(modelPath); - if (materialMeshes.empty()) { - std::cerr << "No material meshes found in loaded model: " << modelPath << std::endl; - return false; - } - - // Collect all geometry entities so we can batch Vulkan uploads for their meshes - std::vector geometryEntities; - geometryEntities.reserve(materialMeshes.size()); - - for (const auto& materialMesh : materialMeshes) { - // Create an entity name based on model and material - std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + - "_" + materialMesh.materialName; - - if (Entity* materialEntity = engine->CreateEntity(entityName)) { - // Add a transform component with provided parameters - auto* transform = materialEntity->AddComponent(); - transform->SetPosition(position); - transform->SetRotation(glm::radians(rotation)); - transform->SetScale(scale); - - // Add a mesh component with material-specific data - auto* mesh = materialEntity->AddComponent(); - mesh->SetVertices(materialMesh.vertices); - mesh->SetIndices(materialMesh.indices); - - if (materialMesh.GetInstanceCount() > 0) { - const std::vector& instances = materialMesh.instances; - for (const auto& instanceData : instances) { - // Reconstruct the transformation matrix from InstanceData column vectors - glm::mat4 instanceMatrix = instanceData.getModelMatrix(); - mesh->AddInstance(instanceMatrix, static_cast(materialMesh.materialIndex)); - } - } - - // Set ALL PBR texture paths for this material - // Set primary texture path for backward compatibility - if (!materialMesh.texturePath.empty()) { - mesh->SetTexturePath(materialMesh.texturePath); - } - - // Set all PBR texture paths - if (!materialMesh.baseColorTexturePath.empty()) { - mesh->SetBaseColorTexturePath(materialMesh.baseColorTexturePath); - } - if (!materialMesh.normalTexturePath.empty()) { - mesh->SetNormalTexturePath(materialMesh.normalTexturePath); - } - if (!materialMesh.metallicRoughnessTexturePath.empty()) { - mesh->SetMetallicRoughnessTexturePath(materialMesh.metallicRoughnessTexturePath); - } - if (!materialMesh.occlusionTexturePath.empty()) { - mesh->SetOcclusionTexturePath(materialMesh.occlusionTexturePath); - } - if (!materialMesh.emissiveTexturePath.empty()) { - mesh->SetEmissiveTexturePath(materialMesh.emissiveTexturePath); - } - - // Fallback: Use material DB (from ModelLoader) if any PBR texture is still missing - if (modelLoader) { - Material* mat = modelLoader->GetMaterial(materialMesh.materialName); - if (mat) { - if (mesh->GetBaseColorTexturePath().empty() && !mat->albedoTexturePath.empty()) { - mesh->SetBaseColorTexturePath(mat->albedoTexturePath); - } - if (mesh->GetNormalTexturePath().empty() && !mat->normalTexturePath.empty()) { - mesh->SetNormalTexturePath(mat->normalTexturePath); - } - if (mesh->GetMetallicRoughnessTexturePath().empty() && !mat->metallicRoughnessTexturePath.empty()) { - mesh->SetMetallicRoughnessTexturePath(mat->metallicRoughnessTexturePath); - } - if (mesh->GetOcclusionTexturePath().empty() && !mat->occlusionTexturePath.empty()) { - mesh->SetOcclusionTexturePath(mat->occlusionTexturePath); - } - if (mesh->GetEmissiveTexturePath().empty() && !mat->emissiveTexturePath.empty()) { - mesh->SetEmissiveTexturePath(mat->emissiveTexturePath); - } - } - } - - // Register all effective texture IDs this mesh uses so that when - // textures finish streaming in, the renderer can refresh - // descriptor sets for the appropriate entities. This must - // happen *after* material fallbacks so we see the final IDs. - if (renderer) { - auto registerTex = [&](const std::string& texId) { - if (!texId.empty()) { - renderer->RegisterTextureUser(texId, materialEntity); - } - }; - - registerTex(mesh->GetTexturePath()); - registerTex(mesh->GetBaseColorTexturePath()); - registerTex(mesh->GetNormalTexturePath()); - registerTex(mesh->GetMetallicRoughnessTexturePath()); - registerTex(mesh->GetOcclusionTexturePath()); - registerTex(mesh->GetEmissiveTexturePath()); - } - - // Track this entity for batched Vulkan resource pre-allocation later - geometryEntities.push_back(materialEntity); - - // Create physics body for collision with balls, but only for geometry - // that is reasonably close to the ground plane. This avoids creating - // expensive mesh colliders for high-up roofs and distant details. - PhysicsSystem* physicsSystem = engine->GetPhysicsSystem(); - if (physicsSystem) { - auto* mc = materialEntity->GetComponent(); - if (mc && !mc->GetVertices().empty() && !mc->GetIndices().empty()) { - // Compute a simple Y-range in WORLD space using the entity transform - // and the mesh's local AABB if available; otherwise approximate from vertices. - glm::vec3 minWS( std::numeric_limits::max()); - glm::vec3 maxWS(-std::numeric_limits::max()); - - auto* xform = materialEntity->GetComponent(); - glm::mat4 model = xform ? xform->GetModelMatrix() : glm::mat4(1.0f); - - if (mc->HasLocalAABB()) { - glm::vec3 localMin = mc->GetLocalAABBMin(); - glm::vec3 localMax = mc->GetLocalAABBMax(); - - // Transform the 8 corners of the local AABB to world space - for (int ix = 0; ix < 2; ++ix) { - for (int iy = 0; iy < 2; ++iy) { - for (int iz = 0; iz < 2; ++iz) { - glm::vec3 corner( - ix ? localMax.x : localMin.x, - iy ? localMax.y : localMin.y, - iz ? localMax.z : localMin.z - ); - glm::vec3 cWS = glm::vec3(model * glm::vec4(corner, 1.0f)); - minWS = glm::min(minWS, cWS); - maxWS = glm::max(maxWS, cWS); - } - } - } - } else { - // Fallback: compute bounds directly from vertices in world space - const auto& verts = mc->GetVertices(); - for (const auto& v : verts) { - glm::vec3 pWS = glm::vec3(model * glm::vec4(v.position, 1.0f)); - minWS = glm::min(minWS, pWS); - maxWS = glm::max(maxWS, pWS); - } - } - - // If we have a valid Y range and the mesh comes within 6 meters of the ground, - // create a physics body. Otherwise, skip it to save startup time and memory. - const float groundY = 0.0f; - const float maxDistanceFromGround = 6.0f; - bool nearGround = (minWS.y <= groundY + maxDistanceFromGround); - - if (nearGround) { - physicsSystem->EnqueueRigidBodyCreation( - materialEntity, - CollisionShape::Mesh, - 0.0f, // mass 0 = static - true, // kinematic - 0.15f, // restitution - 0.5f // friction - ); - std::cout << "Queued physics body for near-ground geometry entity: " << entityName << std::endl; - } else { - std::cout << "Skipped physics body for high/remote entity: " << entityName - << " (minY=" << minWS.y << ")" << std::endl; - } - } else { - std::cerr << "Skipping physics body for entity (no geometry): " << entityName << std::endl; - } - } - - } else { - std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; - } - } - - // Pre-allocate Vulkan resources for all geometry entities in a single batched pass - if (!geometryEntities.empty()) { - if (!renderer->preAllocateEntityResourcesBatch(geometryEntities)) { - std::cerr << "Failed to pre-allocate resources for one or more geometry entities in batch" << std::endl; - // For now, continue; individual entities may still be partially usable - } - } - } catch (const std::exception& e) { - std::cerr << "Error loading GLTF model: " << e.what() << std::endl; - return false; - } - return true; +bool LoadGLTFModel(Engine *engine, const std::string &modelPath, + const glm::vec3 &position, const glm::vec3 &rotation, const glm::vec3 &scale) +{ + // Get the model loader and renderer + ModelLoader *modelLoader = engine->GetModelLoader(); + Renderer *renderer = engine->GetRenderer(); + + if (!modelLoader || !renderer) + { + std::cerr << "Error: ModelLoader or Renderer is null" << std::endl; + if (renderer) + { + renderer->SetLoading(false); + } + return false; + } + // Ensure loading flag is cleared on any exit from this function + struct LoadingGuard + { + Renderer *r; + ~LoadingGuard() + { + r->SetLoading(false); + } + } loadingGuard{renderer}; + + // Extract model name from file path for entity naming + std::filesystem::path modelFilePath(modelPath); + std::string modelName = modelFilePath.stem().string(); // Get filename without extension + + try + { + // Load the complete GLTF model with all textures and lighting on the main thread + Model *loadedModel = modelLoader->LoadGLTF(modelPath); + if (!loadedModel) + { + std::cerr << "Failed to load GLTF model: " << modelPath << std::endl; + return false; + } + + std::cout << "Successfully loaded GLTF model with all textures and lighting: " << modelPath << std::endl; + + // Extract lights from the model and transform them to world space + std::vector extractedLights = modelLoader->GetExtractedLights(modelPath); + + // Create a transformation matrix from position, rotation, and scale + glm::mat4 transformMatrix = glm::mat4(1.0f); + transformMatrix = glm::translate(transformMatrix, position); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + transformMatrix = glm::rotate(transformMatrix, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + transformMatrix = glm::scale(transformMatrix, scale); + + // Transform all light positions from local model space to world space + // Also transform the light direction (for directional lights) + glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); + for (auto &light : extractedLights) + { + glm::vec4 worldPos = transformMatrix * glm::vec4(light.position, 1.0f); + light.position = glm::vec3(worldPos); + light.direction = glm::normalize(normalMatrix * light.direction); + } + + renderer->SetStaticLights(extractedLights); + + // Extract and apply cameras from the GLTF model + const std::vector &cameras = loadedModel->GetCameras(); + if (!cameras.empty()) + { + const CameraData &gltfCamera = cameras[0]; // Use the first camera + + // Find or create a camera entity to replace the default one + Entity *cameraEntity = engine->GetEntity("Camera"); + if (!cameraEntity) + { + // Create a new camera entity if none exists + cameraEntity = engine->CreateEntity("Camera"); + if (cameraEntity) + { + cameraEntity->AddComponent(); + cameraEntity->AddComponent(); + } + } + + if (cameraEntity) + { + // Update the camera transform with GLTF data + auto *cameraTransform = cameraEntity->GetComponent(); + if (cameraTransform) + { + // Apply the transformation matrix to the camera position + glm::vec4 worldPos = transformMatrix * glm::vec4(gltfCamera.position, 1.0f); + cameraTransform->SetPosition(glm::vec3(worldPos)); + + // Apply rotation from GLTF camera + glm::vec3 eulerAngles = glm::eulerAngles(gltfCamera.rotation); + cameraTransform->SetRotation(eulerAngles); + } + + // Update the camera component with GLTF properties + auto *camera = cameraEntity->GetComponent(); + if (camera) + { + camera->ForceViewMatrixUpdate(); // Only sets viewMatrixDirty flag, doesn't change camera orientation + if (gltfCamera.isPerspective) + { + camera->SetFieldOfView(glm::degrees(gltfCamera.fov)); // Convert radians to degrees + camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); + if (gltfCamera.aspectRatio > 0.0f) + { + camera->SetAspectRatio(gltfCamera.aspectRatio); + } + } + else + { + // Handle orthographic camera if needed + camera->SetProjectionType(CameraComponent::ProjectionType::Orthographic); + camera->SetOrthographicSize(gltfCamera.orthographicSize, gltfCamera.orthographicSize); + camera->SetClipPlanes(gltfCamera.nearPlane, gltfCamera.farPlane); + } + + // Set this as the active camera + engine->SetActiveCamera(camera); + } + } + } + + // Get the material meshes from the loaded model + const std::vector &materialMeshes = modelLoader->GetMaterialMeshes(modelPath); + if (materialMeshes.empty()) + { + std::cerr << "No material meshes found in loaded model: " << modelPath << std::endl; + return false; + } + + // Collect all geometry entities so we can batch Vulkan uploads for their meshes + std::vector geometryEntities; + geometryEntities.reserve(materialMeshes.size()); + + for (const auto &materialMesh : materialMeshes) + { + // Create an entity name based on model and material + std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + + "_" + materialMesh.materialName; + + if (Entity *materialEntity = engine->CreateEntity(entityName)) + { + // Add a transform component with provided parameters + auto *transform = materialEntity->AddComponent(); + transform->SetPosition(position); + transform->SetRotation(glm::radians(rotation)); + transform->SetScale(scale); + + // Add a mesh component with material-specific data + auto *mesh = materialEntity->AddComponent(); + mesh->SetVertices(materialMesh.vertices); + mesh->SetIndices(materialMesh.indices); + + if (materialMesh.GetInstanceCount() > 0) + { + const std::vector &instances = materialMesh.instances; + for (const auto &instanceData : instances) + { + // Reconstruct the transformation matrix from InstanceData column vectors + glm::mat4 instanceMatrix = instanceData.getModelMatrix(); + mesh->AddInstance(instanceMatrix, static_cast(materialMesh.materialIndex)); + } + } + + // Set ALL PBR texture paths for this material + // Set primary texture path for backward compatibility + if (!materialMesh.texturePath.empty()) + { + mesh->SetTexturePath(materialMesh.texturePath); + } + + // Set all PBR texture paths + if (!materialMesh.baseColorTexturePath.empty()) + { + mesh->SetBaseColorTexturePath(materialMesh.baseColorTexturePath); + } + if (!materialMesh.normalTexturePath.empty()) + { + mesh->SetNormalTexturePath(materialMesh.normalTexturePath); + } + if (!materialMesh.metallicRoughnessTexturePath.empty()) + { + mesh->SetMetallicRoughnessTexturePath(materialMesh.metallicRoughnessTexturePath); + } + if (!materialMesh.occlusionTexturePath.empty()) + { + mesh->SetOcclusionTexturePath(materialMesh.occlusionTexturePath); + } + if (!materialMesh.emissiveTexturePath.empty()) + { + mesh->SetEmissiveTexturePath(materialMesh.emissiveTexturePath); + } + + // Fallback: Use material DB (from ModelLoader) if any PBR texture is still missing + if (modelLoader) + { + Material *mat = modelLoader->GetMaterial(materialMesh.materialName); + if (mat) + { + if (mesh->GetBaseColorTexturePath().empty() && !mat->albedoTexturePath.empty()) + { + mesh->SetBaseColorTexturePath(mat->albedoTexturePath); + } + if (mesh->GetNormalTexturePath().empty() && !mat->normalTexturePath.empty()) + { + mesh->SetNormalTexturePath(mat->normalTexturePath); + } + if (mesh->GetMetallicRoughnessTexturePath().empty() && !mat->metallicRoughnessTexturePath.empty()) + { + mesh->SetMetallicRoughnessTexturePath(mat->metallicRoughnessTexturePath); + } + if (mesh->GetOcclusionTexturePath().empty() && !mat->occlusionTexturePath.empty()) + { + mesh->SetOcclusionTexturePath(mat->occlusionTexturePath); + } + if (mesh->GetEmissiveTexturePath().empty() && !mat->emissiveTexturePath.empty()) + { + mesh->SetEmissiveTexturePath(mat->emissiveTexturePath); + } + } + } + + // Register all effective texture IDs this mesh uses so that when + // textures finish streaming in, the renderer can refresh + // descriptor sets for the appropriate entities. This must + // happen *after* material fallbacks so we see the final IDs. + if (renderer) + { + auto registerTex = [&](const std::string &texId) { + if (!texId.empty()) + { + renderer->RegisterTextureUser(texId, materialEntity); + } + }; + + registerTex(mesh->GetTexturePath()); + registerTex(mesh->GetBaseColorTexturePath()); + registerTex(mesh->GetNormalTexturePath()); + registerTex(mesh->GetMetallicRoughnessTexturePath()); + registerTex(mesh->GetOcclusionTexturePath()); + registerTex(mesh->GetEmissiveTexturePath()); + } + + // Track this entity for batched Vulkan resource pre-allocation later + geometryEntities.push_back(materialEntity); + + // Create physics body for collision with balls, but only for geometry + // that is reasonably close to the ground plane. This avoids creating + // expensive mesh colliders for high-up roofs and distant details. + PhysicsSystem *physicsSystem = engine->GetPhysicsSystem(); + if (physicsSystem) + { + auto *mc = materialEntity->GetComponent(); + if (mc && !mc->GetVertices().empty() && !mc->GetIndices().empty()) + { + // Compute a simple Y-range in WORLD space using the entity transform + // and the mesh's local AABB if available; otherwise approximate from vertices. + glm::vec3 minWS(std::numeric_limits::max()); + glm::vec3 maxWS(-std::numeric_limits::max()); + + auto *xform = materialEntity->GetComponent(); + glm::mat4 model = xform ? xform->GetModelMatrix() : glm::mat4(1.0f); + + if (mc->HasLocalAABB()) + { + glm::vec3 localMin = mc->GetLocalAABBMin(); + glm::vec3 localMax = mc->GetLocalAABBMax(); + + // Transform the 8 corners of the local AABB to world space + for (int ix = 0; ix < 2; ++ix) + { + for (int iy = 0; iy < 2; ++iy) + { + for (int iz = 0; iz < 2; ++iz) + { + glm::vec3 corner( + ix ? localMax.x : localMin.x, + iy ? localMax.y : localMin.y, + iz ? localMax.z : localMin.z); + glm::vec3 cWS = glm::vec3(model * glm::vec4(corner, 1.0f)); + minWS = glm::min(minWS, cWS); + maxWS = glm::max(maxWS, cWS); + } + } + } + } + else + { + // Fallback: compute bounds directly from vertices in world space + const auto &verts = mc->GetVertices(); + for (const auto &v : verts) + { + glm::vec3 pWS = glm::vec3(model * glm::vec4(v.position, 1.0f)); + minWS = glm::min(minWS, pWS); + maxWS = glm::max(maxWS, pWS); + } + } + + // If we have a valid Y range and the mesh comes within 6 meters of the ground, + // create a physics body. Otherwise, skip it to save startup time and memory. + const float groundY = 0.0f; + const float maxDistanceFromGround = 6.0f; + bool nearGround = (minWS.y <= groundY + maxDistanceFromGround); + + if (nearGround) + { + physicsSystem->EnqueueRigidBodyCreation( + materialEntity, + CollisionShape::Mesh, + 0.0f, // mass 0 = static + true, // kinematic + 0.15f, // restitution + 0.5f // friction + ); + std::cout << "Queued physics body for near-ground geometry entity: " << entityName << std::endl; + } + else + { + std::cout << "Skipped physics body for high/remote entity: " << entityName + << " (minY=" << minWS.y << ")" << std::endl; + } + } + else + { + std::cerr << "Skipping physics body for entity (no geometry): " << entityName << std::endl; + } + } + } + else + { + std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; + } + } + + // Pre-allocate Vulkan resources for all geometry entities in a single batched pass + if (!geometryEntities.empty()) + { + if (!renderer->preAllocateEntityResourcesBatch(geometryEntities)) + { + std::cerr << "Failed to pre-allocate resources for one or more geometry entities in batch" << std::endl; + // For now, continue; individual entities may still be partially usable + } + } + + // Set up animations if the model has any + const std::vector &animations = loadedModel->GetAnimations(); + std::cout << "[Animation] Model has " << animations.size() << " animation(s)" << std::flush << std::endl; + if (!animations.empty()) + { + std::cout << "[Animation] Setting up " << animations.size() << " animation(s) for playback" << std::flush << std::endl; + + // Create an animation controller entity + Entity *animController = engine->CreateEntity(modelName + "_AnimController"); + if (animController) + { + auto *animTransform = animController->AddComponent(); + animTransform->SetPosition(position); + + auto *animComponent = animController->AddComponent(); + animComponent->SetAnimations(animations); + + // Build node-to-entity mapping using actual glTF node indices + // Get animated node mesh mappings to link geometry entities to animated nodes + const auto &animatedNodeMeshes = loadedModel->GetAnimatedNodeMeshes(); + + // Get the base transforms for animated nodes + const auto &animatedNodeTransforms = loadedModel->GetAnimatedNodeTransforms(); + + std::cout << "[Animation] Processing " << animatedNodeMeshes.size() << " animated nodes" << std::endl; + + // Build nodeToEntity mapping by creating or finding entities for each animated node + std::unordered_map nodeToEntity; + std::unordered_map meshUsageCount; // Track how many times each mesh is used + + // First pass: count how many animated nodes use each mesh + for (const auto &[nodeIndex, meshIndex] : animatedNodeMeshes) + { + meshUsageCount[meshIndex]++; + } + + // Second pass: create entities for animated nodes + for (const auto &[nodeIndex, meshIndex] : animatedNodeMeshes) + { + std::cout << "[Animation] Processing animated node " << nodeIndex << " with mesh " << meshIndex << std::endl; + + // Find a MaterialMesh with this sourceMeshIndex + const MaterialMesh *sourceMaterialMesh = nullptr; + size_t sourceMaterialMeshIdx = 0; + for (size_t i = 0; i < materialMeshes.size(); ++i) + { + if (materialMeshes[i].sourceMeshIndex == meshIndex) + { + sourceMaterialMesh = &materialMeshes[i]; + sourceMaterialMeshIdx = i; + break; + } + } + + if (!sourceMaterialMesh) + { + std::cerr << "[Animation] WARNING: No MaterialMesh found for animated node " + << nodeIndex << " (mesh " << meshIndex << ")" << std::endl; + continue; + } + + Entity *nodeEntity = nullptr; + + // If this is the first animated node using this mesh, use the existing entity + // For subsequent nodes, create new entities + bool isFirstUse = (nodeToEntity.size() == 0 || + std::none_of(nodeToEntity.begin(), nodeToEntity.end(), + [meshIndex, &animatedNodeMeshes](const auto &pair) { + auto it = animatedNodeMeshes.find(pair.first); + return it != animatedNodeMeshes.end() && it->second == meshIndex; + })); + + if (isFirstUse && sourceMaterialMeshIdx < geometryEntities.size()) + { + // Reuse existing entity for first animated node with this mesh + nodeEntity = geometryEntities[sourceMaterialMeshIdx]; + std::cout << "[Animation] Reusing existing entity for first node " << nodeIndex << std::endl; + + // CRITICAL: Clear any instance data from the reused entity + // If this mesh was set up for instanced rendering, we need to convert it + // to a single non-instanced entity for animation + auto *mesh = nodeEntity->GetComponent(); + if (mesh && mesh->GetInstanceCount() > 0) + { + size_t instanceCount = mesh->GetInstanceCount(); + mesh->ClearInstances(); + std::cout << "[Animation] Cleared " << instanceCount + << " instances from reused entity for animation" << std::endl; + + // Recreate the GPU instance buffer with a single identity instance + // The old buffer still had multiple instances, so we need to update it + if (renderer && !renderer->recreateInstanceBuffer(nodeEntity)) + { + std::cerr << "[Animation] Failed to recreate instance buffer for reused entity" << std::endl; + } + } + } + else + { + // Create a new entity for this animated node (duplicate geometry) + std::string entityName = modelName + "_AnimNode_" + std::to_string(nodeIndex) + + "_Material_" + std::to_string(sourceMaterialMesh->materialIndex); + nodeEntity = engine->CreateEntity(entityName); + + if (nodeEntity) + { + // Add transform component (will be set below) + nodeEntity->AddComponent(); + + // Clone the mesh component from the source MaterialMesh + auto *mesh = nodeEntity->AddComponent(); + mesh->SetVertices(sourceMaterialMesh->vertices); + mesh->SetIndices(sourceMaterialMesh->indices); + + // Copy all texture paths + if (!sourceMaterialMesh->baseColorTexturePath.empty()) + mesh->SetBaseColorTexturePath(sourceMaterialMesh->baseColorTexturePath); + if (!sourceMaterialMesh->normalTexturePath.empty()) + mesh->SetNormalTexturePath(sourceMaterialMesh->normalTexturePath); + if (!sourceMaterialMesh->metallicRoughnessTexturePath.empty()) + mesh->SetMetallicRoughnessTexturePath(sourceMaterialMesh->metallicRoughnessTexturePath); + if (!sourceMaterialMesh->occlusionTexturePath.empty()) + mesh->SetOcclusionTexturePath(sourceMaterialMesh->occlusionTexturePath); + if (!sourceMaterialMesh->emissiveTexturePath.empty()) + mesh->SetEmissiveTexturePath(sourceMaterialMesh->emissiveTexturePath); + + // Register textures with renderer + if (renderer) + { + auto registerTex = [&](const std::string &texId) { + if (!texId.empty()) + renderer->RegisterTextureUser(texId, nodeEntity); + }; + registerTex(mesh->GetBaseColorTexturePath()); + registerTex(mesh->GetNormalTexturePath()); + registerTex(mesh->GetMetallicRoughnessTexturePath()); + registerTex(mesh->GetOcclusionTexturePath()); + registerTex(mesh->GetEmissiveTexturePath()); + } + + // Pre-allocate resources for this new entity + if (renderer && !renderer->preAllocateEntityResources(nodeEntity)) + { + std::cerr << "[Animation] Failed to pre-allocate resources for " << entityName << std::endl; + } + + std::cout << "[Animation] Created new entity '" << entityName << "' for node " << nodeIndex << std::endl; + } + } + + if (nodeEntity) + { + // Apply the base transform from the glTF node to this entity + auto transformIt = animatedNodeTransforms.find(nodeIndex); + if (transformIt != animatedNodeTransforms.end()) + { + const glm::mat4 &nodeTransform = transformIt->second; + + // Decompose the matrix into position, rotation, and scale + glm::vec3 nodePosition, nodeScale, skew; + glm::quat nodeRotation; + glm::vec4 perspective; + glm::decompose(nodeTransform, nodeScale, nodeRotation, nodePosition, skew, perspective); + + // Apply the node's local transform to the entity + auto *transform = nodeEntity->GetComponent(); + if (transform) + { + transform->SetPosition(nodePosition); + transform->SetRotation(glm::eulerAngles(nodeRotation)); + transform->SetScale(nodeScale); + std::cout << "[Animation] Applied base transform to entity '" << nodeEntity->GetName() + << "' - pos(" << nodePosition.x << "," << nodePosition.y << "," << nodePosition.z << ")" << std::endl; + } + } + + nodeToEntity[nodeIndex] = nodeEntity; + std::cout << "[Animation] Linked entity '" << nodeEntity->GetName() + << "' to animated node " << nodeIndex << std::endl; + } + } + + animComponent->SetNodeToEntityMap(nodeToEntity); + + std::cout << "[Animation] Node-to-entity mapping has " << nodeToEntity.size() + << " entries (of " << animatedNodeMeshes.size() << " animated nodes)" << std::endl; + + // Auto-play the first animation + if (!animations.empty()) + { + animComponent->Play(0, true); // Play first animation, looping + std::cout << "Auto-playing animation: " << animations[0].name + << " (duration: " << animations[0].GetDuration() << "s)" << std::endl; + } + } + } + } + catch (const std::exception &e) + { + std::cerr << "Error loading GLTF model: " << e.what() << std::endl; + return false; + } + + // Request acceleration structure build at next safe frame point + // Don't build here in background thread to avoid threading issues with command pools + if (renderer && renderer->GetRayQueryEnabled() && renderer->GetAccelerationStructureEnabled()) + { + std::cout << "Requesting acceleration structure build for loaded scene..." << std::endl; + renderer->RequestAccelerationStructureBuild(); + } + + return true; } /** @@ -336,7 +625,8 @@ bool LoadGLTFModel(Engine* engine, const std::string& modelPath, * @param engine The engine to create entities in. * @param modelPath The path to the GLTF model file. */ -void LoadGLTFModel(Engine* engine, const std::string& modelPath) { - // Use default transform values: slight Y offset, no rotation, unit scale - LoadGLTFModel(engine, modelPath, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)); +void LoadGLTFModel(Engine *engine, const std::string &modelPath) +{ + // Use default transform values: slight Y offset, no rotation, unit scale + LoadGLTFModel(engine, modelPath, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f, 1.0f, 1.0f)); } diff --git a/attachments/simple_engine/scene_loading.h b/attachments/simple_engine/scene_loading.h index dee4f0dc..d79373c1 100644 --- a/attachments/simple_engine/scene_loading.h +++ b/attachments/simple_engine/scene_loading.h @@ -1,9 +1,25 @@ +/* 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 "model_loader.h" +#include +#include +#include // Forward declarations class Engine; @@ -17,12 +33,12 @@ class ModelLoader; * @param rotation The rotation to apply to the model. * @param scale The scale to apply to the model. */ -bool LoadGLTFModel(Engine* engine, const std::string& modelPath, - const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& scale); +bool LoadGLTFModel(Engine *engine, const std::string &modelPath, + const glm::vec3 &position, const glm::vec3 &rotation, const glm::vec3 &scale); /** * @brief Load a GLTF model with default transform values. * @param engine The engine to create entities in. * @param modelPath The path to the GLTF model file. */ -void LoadGLTFModel(Engine* engine, const std::string& modelPath); +void LoadGLTFModel(Engine *engine, const std::string &modelPath); diff --git a/attachments/simple_engine/shaders/common_types.slang b/attachments/simple_engine/shaders/common_types.slang new file mode 100644 index 00000000..a395b8d3 --- /dev/null +++ b/attachments/simple_engine/shaders/common_types.slang @@ -0,0 +1,146 @@ +/* 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)]] int lightType; + [[vk::offset(100)]] float range; + [[vk::offset(104)]] float innerConeAngle; + [[vk::offset(108)]] float outerConeAngle; +}; + +// Uniform buffer object +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 camPos; + 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; +}; + +// 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/simple_engine/shaders/composite.slang b/attachments/simple_engine/shaders/composite.slang new file mode 100644 index 00000000..60138fe1 --- /dev/null +++ b/attachments/simple_engine/shaders/composite.slang @@ -0,0 +1,80 @@ +/* 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 + +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; + +float3 tonemapReinhard(float3 x) +{ + return x / (1.0 + x); +} + +float3 applyExposure(float3 x, float exposure) +{ + return 1.0 - exp(-x * max(exposure, 0.0001)); +} + +float3 linearToGamma(float3 x, float gamma) +{ + float inv = (gamma > 0.0) ? (1.0 / gamma) : (1.0 / 2.2); + return pow(max(x, 0.0), inv); +} + +// Export entrypoint for fragment stage +[shader("fragment")] float4 PSMain(VSOut i) : SV_TARGET +{ + float4 c = sceneColor.Sample(i.UV); + float3 color = c.rgb; + // Simple exposure; optional reinhard if desired later + color = applyExposure(color, pushConsts.exposure); + + // 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 = linearToGamma(color, pushConsts.gamma); + } + return float4(color, 1.0); +} diff --git a/attachments/simple_engine/shaders/forward_plus_cull.slang b/attachments/simple_engine/shaders/forward_plus_cull.slang new file mode 100644 index 00000000..7dc832d7 --- /dev/null +++ b/attachments/simple_engine/shaders/forward_plus_cull.slang @@ -0,0 +1,145 @@ +/* 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) + +struct LightData { + [[vk::offset(0)]] float4 position; + [[vk::offset(16)]] float4 color; + [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; + [[vk::offset(96)]] int lightType; + [[vk::offset(100)]] float range; + [[vk::offset(104)]] float innerConeAngle; + [[vk::offset(108)]] float outerConeAngle; +}; + +struct TileHeader { uint offset; uint count; uint pad0; uint pad1; }; + +// 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/simple_engine/shaders/hrtf.slang b/attachments/simple_engine/shaders/hrtf.slang index f734b106..3ebd59fc 100644 --- a/attachments/simple_engine/shaders/hrtf.slang +++ b/attachments/simple_engine/shaders/hrtf.slang @@ -1,3 +1,19 @@ +/* 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 diff --git a/attachments/simple_engine/shaders/imgui.slang b/attachments/simple_engine/shaders/imgui.slang index 44541432..3d10f7cc 100644 --- a/attachments/simple_engine/shaders/imgui.slang +++ b/attachments/simple_engine/shaders/imgui.slang @@ -1,3 +1,19 @@ +/* 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 diff --git a/attachments/simple_engine/shaders/lighting.slang b/attachments/simple_engine/shaders/lighting.slang index 4837f3d5..5d673cc8 100644 --- a/attachments/simple_engine/shaders/lighting.slang +++ b/attachments/simple_engine/shaders/lighting.slang @@ -1,3 +1,19 @@ +/* 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 diff --git a/attachments/simple_engine/shaders/lighting_utils.slang b/attachments/simple_engine/shaders/lighting_utils.slang new file mode 100644 index 00000000..46f0975e --- /dev/null +++ b/attachments/simple_engine/shaders/lighting_utils.slang @@ -0,0 +1,101 @@ +/* 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 + result.radiance = light.color.rgb / max(d * d, 0.0001); + 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/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index fc4c6a59..7b67e9f4 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -1,3 +1,25 @@ +/* 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; @@ -24,62 +46,8 @@ struct VSOutput { float4 Tangent : TANGENT; }; -// 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)]] int lightType; - [[vk::offset(100)]] float range; - [[vk::offset(104)]] float innerConeAngle; - [[vk::offset(108)]] float outerConeAngle; -}; - -// Uniform buffer (now without fixed light arrays) -struct UniformBufferObject { - float4x4 model; - float4x4 view; - float4x4 proj; - float4 camPos; - float exposure; - float gamma; - float prefilteredCubeMipLevels; - float scaleIBLAmbient; - int lightCount; - int padding0; - float padding1; - float padding2; - float2 screenDimensions; -}; - - [[vk::binding(0, 1)]] Sampler2D opaqueSceneColor; -// 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; -}; - -// Constants -static const float PI = 3.14159265359; - // Bindings [[vk::binding(0, 0)]] ConstantBuffer ubo; [[vk::binding(1, 0)]] Sampler2D baseColorMap; @@ -88,38 +56,14 @@ static const float PI = 3.14159265359; [[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; [[vk::push_constant]] PushConstants material; -// PBR functions -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); -} - -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; -} - -float3 FresnelSchlick(float cosTheta, float3 F0) { - return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); -} - -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); -} - // Vertex shader entry point [[shader("vertex")]] VSOutput VSMain(VSInput input) @@ -130,39 +74,23 @@ VSOutput VSMain(VSInput input) output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); output.WorldPos = worldPos.xyz; - // DEBUG EXPERIMENT: - // Ignore the per-instance normal matrix columns (InstanceNormal0/1/2) - // and derive normals purely from the combined entity * instance - // transform. This helps isolate whether InstanceNormal is the - // source of the discrepancy between nearby ground patches. - float3x3 combined3x3 = (float3x3)mul(ubo.model, instanceModelMatrix); - - float3 worldNormal = normalize(mul(combined3x3, input.Normal)); + // 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 combined transform. - float3 geomNormal = worldNormal; - output.GeometricNormal = geomNormal; + // Geometric normal (pre-normal-map) uses the same transform path. + output.GeometricNormal = worldNormal; - // For this test, transform tangents with the same combined3x3 so - // the TBN basis is consistent with the position transform, while - // still bypassing InstanceNormal{0,1,2}. - float3 worldTangent = normalize(mul(combined3x3, input.Tangent.xyz)); + // 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; } -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; - } -} - // Fragment shader entry point for generic PBR materials [[shader("fragment")]] float4 PSMain(VSOutput input) : SV_TARGET @@ -200,9 +128,7 @@ float4 PSMain(VSOutput input) : SV_TARGET if (material.hasEmissiveStrengthExt) emissive *= material.emissiveStrength; - if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { - discard; - } + if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { discard; } // --- 2. Normal Calculation --- float3 N = normalize(input.Normal); @@ -231,48 +157,129 @@ float4 PSMain(VSOutput input) : SV_TARGET 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.view, 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; + + uint base = 0u; + uint count = 0u; + if (tileId < totalTiles * (uint)slices) { + TileHeader th = tileHeaders[tileId]; + base = th.offset; + count = th.count; + } + + bool forceGlobal = false; + + // No one-frame fragment debug + // Accumulate per-light diffuse and specular terms using GGX microfacet BRDF. - for (int i = 0; i < ubo.lightCount; i++) { - LightData light = lightBuffer[i]; - float3 L, radiance; - if (light.lightType == 1) { - L = normalize(-light.position.xyz); - radiance = light.color.rgb; - } else { - // Point/spot/emissive: position.xyz is light position in world space - L = normalize(light.position.xyz - input.WorldPos); - float d = length(light.position.xyz - input.WorldPos); - radiance = light.color.rgb / max(d * d, 0.0001); - - // For emissive-derived lights (lightType == 3), skip lights whose - // contribution has attenuated to a numerically negligible level at - // this fragment. This avoids paying full BRDF cost for thousands - // of far-away emissive sources while preserving the visible ones. - if (light.lightType == 3) { - float radianceLuma = dot(radiance, float3(0.299, 0.587, 0.114)); - if (radianceLuma < 1e-4) { - continue; + if (!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; + 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); + + if (light.lightType == 3) { + // Emissive: soft falloff using range as a characteristic radius + float r = max(light.range, 0.001); + float att = 1.0 / (1.0 + (d / r) * (d / r)); + radiance = light.color.rgb * att; + } else { + // Punctual (not used currently) + radiance = light.color.rgb / max(d * d, 0.0001); } } + // 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) { + 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; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + // no debug writes + } } - float NdotL = max(dot(N, L), 0.0); - if (NdotL > 0.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; - diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + } + // 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 (forceGlobal || (count == 0 && ubo.lightCount > 0 && (ubo.padding1 == 0.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; + 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); + 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 NdotL = (light.lightType == 3) ? abs(dot(N, L)) : max(dot(N, L), 0.0); + if (NdotL > 0.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; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + } } } - // specularLighting = min(specularLighting, 50.0); + + // No debug paths float3 ambient = albedo * ao * (0.03 * ubo.scaleIBLAmbient); float3 opaqueLit = diffuseLighting + specularLighting + ambient + emissive; @@ -280,6 +287,15 @@ float4 PSMain(VSOutput input) : SV_TARGET 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 --- // Apply exposure and filmic tonemapping; apply gamma only if the swapchain is NOT sRGB. color *= ubo.exposure; @@ -356,11 +372,28 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET float alphaOut = baseColor.a; if (T_eff > 0.0) { - // Stylized, environment-independent glass: - // - Do NOT sample opaqueSceneColor. - // - Treat glass as a tinted, view-dependent surface with a subtle - // rim highlight. This keeps behavior stable while still giving - // users a sense of thickness and angle. + // Reflections for glass + // Prefer planar reflection texture rendered from the mirrored view when enabled. + // Fallback: use the opaque scene color behind the glass (refraction/background approximation). + float3 refl = float3(0.0, 0.0, 0.0); + 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; + // Sample only if within the reflection RT + if (uvP.x >= 0.0 && uvP.x <= 1.0 && uvP.y >= 0.0 && uvP.y <= 1.0) { + refl = reflectionMap.Sample(uvP).rgb; + } + } else { + // Project current position to screen for a stable background sample + float2 ndc = input.Position.xy / input.Position.w; + float2 uvR = ndc * 0.5 + 0.5; + uvR = clamp(uvR, float2(0.0, 0.0), float2(1.0, 1.0)); + refl = opaqueSceneColor.Sample(uvR).rgb; + } + + // Stylized, stable glass: do NOT sample opaqueSceneColor. 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). @@ -388,9 +421,16 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET color = glassBody + rim + surfaceTerm; - // Fresnel only influences alpha (how opaque the glass appears), not color. - float3 F_view = FresnelSchlick(NdotV, float3(0.04, 0.04, 0.04)); - float F_avg = (F_view.r + F_view.g + F_view.b) / 3.0; + // 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.15 + (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 @@ -402,6 +442,9 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET float baseAlpha = lerp(0.08, 0.25, 1.0 - T_eff); // Edge boost: more opaque at grazing angles, scaled by Fresnel. + // Recompute a local Fresnel average for alpha since we’re in debug mode above. + float3 F_view_dbg = FresnelSchlick(NdotV, float3(0.04, 0.04, 0.04)); + float F_avg = (F_view_dbg.r + F_view_dbg.g + F_view_dbg.b) / 3.0; float edgeFactor = pow(1.0 - NdotV, 2.0); float edgeAlpha = F_avg * edgeFactor * 0.8; @@ -412,7 +455,74 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET color = ambient + 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.camPos.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; + 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); + 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) { + 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; + } + } + } + } + + // --- 3. Post-processing (same as PSMain) --- + // No debug recording color *= ubo.exposure; // Uncharted2 / Hable filmic tonemap. Use the canonical form without @@ -428,5 +538,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET color = saturate(color); } + // (fragment debug disabled in this build) + return float4(color, alphaOut); -} +} \ No newline at end of file diff --git a/attachments/simple_engine/shaders/pbr_utils.slang b/attachments/simple_engine/shaders/pbr_utils.slang new file mode 100644 index 00000000..59846a20 --- /dev/null +++ b/attachments/simple_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/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang index 82822970..31882bf8 100644 --- a/attachments/simple_engine/shaders/physics.slang +++ b/attachments/simple_engine/shaders/physics.slang @@ -1,3 +1,19 @@ +/* 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 diff --git a/attachments/simple_engine/shaders/ray_query.slang b/attachments/simple_engine/shaders/ray_query.slang new file mode 100644 index 00000000..f99719ff --- /dev/null +++ b/attachments/simple_engine/shaders/ray_query.slang @@ -0,0 +1,849 @@ +/* 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; + +// GPU data structures for proper geometry and material access +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]; +}; + +// 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) + [[vk::offset(264)]] int _pad1; // reserved +}; + +// 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.001; + +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) { + float3 base = max(hit.color, 0.0); + int maxBounces = clamp(ubo._pad0, 0, 10); + if (maxBounces <= 0) { + return base; + } + + 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); + float T = 0.0; + if (ubo.enableRayQueryTransparency != 0 && !hit.isLiquid) { + // Do not force large T for glass; rely on authored/heuristic transmission. + // This avoids energy gain that can make glass appear opaque/overbright. + T = max(physicalTransmission, blendTransmission); + } + + // 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; + float3 rd = normalize(reflect(rayDir, N)); + // Bias along the secondary ray direction to avoid self-intersection. + ro += rd * RQ_RAY_EPS; + + float3 last = skyColor(rd); + for (int b = 0; b < maxBounces; ++b) { + HitInfo rh = traceRay(ro, rd, RQ_RAY_EPS, 10000.0); + 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 + rd * RQ_RAY_EPS; + } + reflCol = last; + } + + // Transmission ray: + // - Physical transmission / glass: thin refraction ray + // - Alpha BLEND with no physical transmission: straight-through ray (no refraction) + float3 thruCol = float3(0.0, 0.0, 0.0); + bool doThru = (T > 1e-4); + if (doThru) { + float3 thruDir = rayDir; + + if (physicalTransmission > 1e-4 || hit.isGlass) { + // Thin refraction for glass/transmission + float3 Nn = N; + float eta = 1.0 / max(hit.ior, 1.0); + // Determine if we're entering or exiting based on ray direction vs normal + if (dot(rayDir, N) > 0.0) { + Nn = -N; + eta = max(hit.ior, 1.0); + } + float3 refrDir; + if (refract(rayDir, Nn, eta, refrDir)) { + thruDir = normalize(refrDir); + } else { + // Total internal reflection + doThru = false; + } + } + + if (doThru) { + // We want the transmitted view of the scene BEYOND the glass exit surface, + // not the color of the glass backface itself. So: + // 1) Trace from just inside the entry point to find the exit surface on the same instance + // 2) If found, trace again from just beyond the exit point to sample the true background + // 3) If not found, fall back to the first trace color (behaves like thin surface) + + float3 startPos = hit.worldPos + thruDir * RQ_RAY_EPS; + HitInfo firstHit = traceRay(startPos, thruDir, RQ_RAY_EPS, 10000.0); + + // Attempt to find exit on the same instance (backface) + HitInfo exitHit = traceRay(startPos, thruDir, RQ_RAY_EPS, 10000.0); + bool haveExitSameInstance = exitHit.hit && (exitHit.instanceId == hit.instanceId); + + // Sample the scene beyond the exit if available, otherwise use firstHit + if (haveExitSameInstance) { + float3 exitPos = exitHit.worldPos + thruDir * RQ_RAY_EPS; + HitInfo beyond = traceRay(exitPos, thruDir, RQ_RAY_EPS, 10000.0); + thruCol = beyond.hit ? max(beyond.color, 0.0) : skyColor(thruDir); + } else { + thruCol = firstHit.hit ? max(firstHit.color, 0.0) : skyColor(thruDir); + } + + // Base color tint (thin-glass default behavior) + if (hit.isGlass || physicalTransmission > 1e-4) { + thruCol *= clamp(hit.baseColor, 0.0, 1.0); + } + + // Volumetric absorption for THICK glass (skip for thin-walled) + if (ubo.enableThickGlass != 0 && !hit.thinWalled && (hit.isGlass || physicalTransmission > 1e-4)) { + float thickness = 0.0; + if (haveExitSameInstance) { + thickness = distance(exitHit.worldPos, hit.worldPos); + } else { + // Fallback small thickness if not watertight + thickness = 0.01; // 1 cm + } + // Clamp to avoid over-darkening from outliers + thickness = min(thickness, max(0.0, ubo.thicknessClamp)); + + if (thickness > 1e-6) { + // Convert absorptionColor to sigma_a using Beer–Lambert: C = exp(-sigma*D) => sigma = -ln(C)/D + uint mi = min(hit.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); + float3 Tvol = exp(-sigma_a * thickness); + thruCol *= saturate(Tvol); + } + } + + // Refractive radiance compensation to mitigate perceived amplification. + // Our refract() call used eta = n1/n2. Scale transmitted radiance by + // min(1, (n2/n1)^2) = min(1, 1/(eta^2)) when ENTERING a denser medium. + // Do not amplify when exiting; clamp to 1.0 to avoid brightening. + { + // Recreate the n1/n2 used for refract(): if ray is entering (front-face), eta = n1/n2 = 1/ior; else eta = ior + bool entering = (dot(rayDir, N) < 0.0); + float eta_n1_over_n2 = entering ? (1.0 / max(hit.ior, 1.0)) : max(hit.ior, 1.0); + if (entering) { + float invEta2 = 1.0 / max(eta_n1_over_n2 * eta_n1_over_n2, 1e-4); + float transScale = clamp(invEta2, 0.0, 1.0); + thruCol *= transScale; + } + } + } + } + + if (T > 1e-4) { + // Transmissive: 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; + + // For alpha BLEND without physical transmission, composite with base using coverage. + if (hit.alphaMode == 2 && physicalTransmission <= 1e-4 && !hit.isGlass) { + return base * opacity + mixed * (1.0 - opacity); + } + + // Glass/transmission path: return the energy-conserving mixture directly. + return mixed; + } + + // Opaque: add a controlled reflection contribution (avoids double-counting too much) + float reflWeight = Fr * (1.0 - clamp(hit.roughness, 0.0, 1.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) { + float alpha = material.alpha; + if (material.baseColorTextureSet >= 0) { + 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; +} + +// 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) { + 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]; + + bool accept = true; + if (matC.alphaMode == 1) { + float alpha = computeBaseColorAlpha(matC, instIndex, q.CandidatePrimitiveIndex(), q.CandidateTriangleBarycentrics()); + accept = (alpha >= matC.alphaCutoff); + } + + if (accept) { + q.CommitNonOpaqueTriangleHit(); + } + } + } + } + + // (No debugPrintf in production) + + // 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(); + + // Minimal debug disabled in production + + // 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]; + + // CRITICAL: Validate vertex indices are within bounds to prevent GPU hang + // Reading beyond vertex buffer bounds causes GPU fault/hang + 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); + if (material.baseColorTextureSet >= 0) { + uint tiBase = (uint)min(max(material.baseColorTexIndex, 0), int(RQ_MAX_TEX - 1)); + baseColor = baseColorTex[NonUniformResourceIndex(tiBase)].SampleLevel(uvSample, lodHint); + baseColor *= float4(material.albedo, material.alpha); + result.texIndex = tiBase; + } else { + result.texIndex = 0u; + } + + result.baseColor = saturate(baseColor.rgb); + + result.alphaMode = material.alphaMode; + result.opacity = clamp(baseColor.a, 0.0, 1.0); + + // 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(ubo.camPos.xyz - result.worldPos); + float3 diffuseLighting = float3(0.0, 0.0, 0.0); + float3 specularLighting = float3(0.0, 0.0, 0.0); + + int lc = max(ubo.lightCount, 0); + for (int li = 0; li < lc; ++li) { + LightData light = lightBuffer[li]; + float3 L; + float3 radiance; + if (light.lightType == 1) { + L = normalize(-light.position.xyz); + radiance = light.color.rgb; + } else { + float3 toLight = light.position.xyz - result.worldPos; + float d = length(toLight); + L = (d > 1e-5) ? (toLight / d) : float3(0, 0, 1); + 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(N, L); + float NdotL = (light.lightType == 3) ? abs(rawDot) : max(rawDot, 0.0); + if (NdotL > 0.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; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + } + } + + float3 ambient = albedo * (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; + result.opacity = clamp(material.alpha, 0.0, 1.0); + result.thinWalled = (material.thinWalled != 0); + + // (No debug-only overrides in production) + } + + 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; + } + + // (No debug-only heartbeat/screen-test paths in production) + + // 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); + + HitInfo hit = traceRay(rayOrigin, rayDir, 0.0001, 10000.0); + + // Production: normal shading (no instance visualization) + bool colorByInstance = false; + + if (hit.hit) { + float3 c = shadeWithSecondaryRays(rayOrigin, rayDir, hit); + // Output linear HDR-ish color; composite pass will apply exposure/gamma. + outputImage[pixelCoord] = float4(c, 1.0); + + // (Debug print hooks intentionally removed from default path) + } else { + // Sky/background color + outputImage[pixelCoord] = float4(skyColor(rayDir), 1.0); + } + // Debug probes disabled in production +} diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang index 8e6cfce2..128ee1c8 100644 --- a/attachments/simple_engine/shaders/texturedMesh.slang +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -1,3 +1,19 @@ +/* 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 diff --git a/attachments/simple_engine/shaders/tonemapping_utils.slang b/attachments/simple_engine/shaders/tonemapping_utils.slang new file mode 100644 index 00000000..33646b79 --- /dev/null +++ b/attachments/simple_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/simple_engine/swap_chain.h b/attachments/simple_engine/swap_chain.h index 50ede2fa..4237df78 100644 --- a/attachments/simple_engine/swap_chain.h +++ b/attachments/simple_engine/swap_chain.h @@ -1,99 +1,131 @@ +/* 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 "vulkan_device.h" #include "platform.h" +#include "vulkan_device.h" /** * @brief Class for managing the Vulkan swap chain. */ -class SwapChain { -public: - /** - * @brief Constructor. - * @param device The Vulkan device. - * @param platform The platform. - */ - SwapChain(VulkanDevice& device, Platform* platform); - - /** - * @brief Destructor. - */ - ~SwapChain(); - - /** - * @brief Create the swap chain. - * @return True if the swap chain was created successfully, false otherwise. - */ - bool create(); - - /** - * @brief Create image views for the swap chain images. - * @return True if the image views were created successfully, false otherwise. - */ - bool createImageViews(); - - /** - * @brief Clean up the swap chain. - */ - void cleanup(); - - /** - * @brief Recreate the swap chain. - * @return True, if the swap chain was recreated successfully, false otherwise. - */ - bool recreate(); - - /** - * @brief Get the swap chain. - * @return The swap chain. - */ - vk::raii::SwapchainKHR& getSwapChain() { return swapChain; } - - /** - * @brief Get the swap chain images. - * @return The swap chain images. - */ - const std::vector& getSwapChainImages() const { return swapChainImages; } - - /** - * @brief Get the swap chain image format. - * @return The swap chain image format. - */ - vk::Format getSwapChainImageFormat() const { return swapChainImageFormat; } - - /** - * @brief Get the swap chain extent. - * @return The swap chain extent. - */ - vk::Extent2D getSwapChainExtent() const { return swapChainExtent; } - - /** - * @brief Get the swap chain image views. - * @return The swap chain image views. - */ - const std::vector& getSwapChainImageViews() const { return swapChainImageViews; } - -private: - // Vulkan device - VulkanDevice& device; - - // Platform - Platform* platform; - - // Swap chain - vk::raii::SwapchainKHR swapChain = nullptr; - std::vector swapChainImages; - vk::Format swapChainImageFormat = vk::Format::eUndefined; - vk::Extent2D swapChainExtent = {0, 0}; - std::vector swapChainImageViews; - - // Helper functions - vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats); - vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes); - vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); +class SwapChain +{ + public: + /** + * @brief Constructor. + * @param device The Vulkan device. + * @param platform The platform. + */ + SwapChain(VulkanDevice &device, Platform *platform); + + /** + * @brief Destructor. + */ + ~SwapChain(); + + /** + * @brief Create the swap chain. + * @return True if the swap chain was created successfully, false otherwise. + */ + bool create(); + + /** + * @brief Create image views for the swap chain images. + * @return True if the image views were created successfully, false otherwise. + */ + bool createImageViews(); + + /** + * @brief Clean up the swap chain. + */ + void cleanup(); + + /** + * @brief Recreate the swap chain. + * @return True, if the swap chain was recreated successfully, false otherwise. + */ + bool recreate(); + + /** + * @brief Get the swap chain. + * @return The swap chain. + */ + vk::raii::SwapchainKHR &getSwapChain() + { + return swapChain; + } + + /** + * @brief Get the swap chain images. + * @return The swap chain images. + */ + const std::vector &getSwapChainImages() const + { + return swapChainImages; + } + + /** + * @brief Get the swap chain image format. + * @return The swap chain image format. + */ + vk::Format getSwapChainImageFormat() const + { + return swapChainImageFormat; + } + + /** + * @brief Get the swap chain extent. + * @return The swap chain extent. + */ + vk::Extent2D getSwapChainExtent() const + { + return swapChainExtent; + } + + /** + * @brief Get the swap chain image views. + * @return The swap chain image views. + */ + const std::vector &getSwapChainImageViews() const + { + return swapChainImageViews; + } + + private: + // Vulkan device + VulkanDevice &device; + + // Platform + Platform *platform; + + // Swap chain + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent = {0, 0}; + std::vector swapChainImageViews; + + // Helper functions + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats); + vk::PresentModeKHR chooseSwapPresentMode(const std::vector &availablePresentModes); + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabilities); }; diff --git a/attachments/simple_engine/thread_pool.h b/attachments/simple_engine/thread_pool.h index 6ae022ac..a8cf913c 100644 --- a/attachments/simple_engine/thread_pool.h +++ b/attachments/simple_engine/thread_pool.h @@ -1,86 +1,115 @@ +/* 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 // Generic reusable thread pool for background tasks (texture uploads, geometry processing, etc.) -class ThreadPool { -public: - explicit ThreadPool(size_t threadCount = std::thread::hardware_concurrency()) - : stopFlag(false) { - if (threadCount == 0) threadCount = 1; - for (size_t i = 0; i < threadCount; ++i) { - workers.emplace_back([this]() { this->workerLoop(); }); - } - } +class ThreadPool +{ + public: + explicit ThreadPool(size_t threadCount = std::thread::hardware_concurrency()) : + stopFlag(false) + { + if (threadCount == 0) + threadCount = 1; + for (size_t i = 0; i < threadCount; ++i) + { + workers.emplace_back([this]() { this->workerLoop(); }); + } + } - ~ThreadPool() { - shutdown(); - } + ~ThreadPool() + { + shutdown(); + } - template - auto enqueue(F&& f, Args&&... args) -> std::future::type> { - using return_type = typename std::invoke_result::type; + template + auto enqueue(F &&f, Args &&...args) -> std::future::type> + { + using return_type = typename std::invoke_result::type; - auto task = std::make_shared>( - [func = std::decay_t(std::forward(f)), - tup = std::make_tuple(std::forward(args)...)]() mutable -> return_type { - return std::apply(std::move(func), std::move(tup)); - } - ); + auto task = std::make_shared>( + [func = std::decay_t(std::forward(f)), + tup = std::make_tuple(std::forward(args)...)]() mutable -> return_type { + return std::apply(std::move(func), std::move(tup)); + }); - std::future res = task->get_future(); - { - std::unique_lock lock(queueMutex); - if (stopFlag) { - throw std::runtime_error("enqueue on stopped ThreadPool"); - } - tasks.emplace([task]() { (*task)(); }); - } - condVar.notify_one(); - return res; - } + std::future res = task->get_future(); + { + std::unique_lock lock(queueMutex); + if (stopFlag) + { + throw std::runtime_error("enqueue on stopped ThreadPool"); + } + tasks.emplace([task]() { (*task)(); }); + } + condVar.notify_one(); + return res; + } - void shutdown() { - { - std::unique_lock lock(queueMutex); - if (stopFlag) return; - stopFlag = true; - } - condVar.notify_all(); - for (auto& t : workers) { - if (t.joinable()) t.join(); - } - workers.clear(); - } + void shutdown() + { + { + std::unique_lock lock(queueMutex); + if (stopFlag) + return; + stopFlag = true; + } + condVar.notify_all(); + for (auto &t : workers) + { + if (t.joinable()) + t.join(); + } + workers.clear(); + } -private: - void workerLoop() { - for (;;) { - std::function task; - { - std::unique_lock lock(queueMutex); - condVar.wait(lock, [this]() { return stopFlag || !tasks.empty(); }); - if (stopFlag && tasks.empty()) return; - task = std::move(tasks.front()); - tasks.pop(); - } - task(); - } - } + private: + void workerLoop() + { + for (;;) + { + std::function task; + { + std::unique_lock lock(queueMutex); + condVar.wait(lock, [this]() { return stopFlag || !tasks.empty(); }); + if (stopFlag && tasks.empty()) + return; + task = std::move(tasks.front()); + tasks.pop(); + } + task(); + } + } - std::vector workers; - std::queue> tasks; - std::mutex queueMutex; - std::condition_variable condVar; - std::atomic stopFlag; + std::vector workers; + std::queue> tasks; + std::mutex queueMutex; + std::condition_variable condVar; + std::atomic stopFlag; }; diff --git a/attachments/simple_engine/transform_component.cpp b/attachments/simple_engine/transform_component.cpp index b25ffae5..56d2ef34 100644 --- a/attachments/simple_engine/transform_component.cpp +++ b/attachments/simple_engine/transform_component.cpp @@ -1,3 +1,19 @@ +/* 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 "transform_component.h" // Most of the TransformComponent class implementation is in the header file @@ -8,24 +24,27 @@ // Returns the model matrix, updating it if necessary // @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix -const glm::mat4& TransformComponent::GetModelMatrix() { - if (matrixDirty) { - UpdateModelMatrix(); - } - return modelMatrix; +const glm::mat4 &TransformComponent::GetModelMatrix() +{ + if (matrixDirty) + { + UpdateModelMatrix(); + } + return modelMatrix; } // Updates the model matrix based on position, rotation, and scale // @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix -void TransformComponent::UpdateModelMatrix() { - // Compose rotation with quaternions for stability and to avoid rad/deg ambiguity - glm::mat4 T = glm::translate(glm::mat4(1.0f), position); - glm::quat qx = glm::angleAxis(rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); - glm::quat qy = glm::angleAxis(rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); - glm::quat qz = glm::angleAxis(rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); - glm::quat q = qz * qy * qx; // ZYX order is conventional for Euler composition - glm::mat4 R = glm::mat4_cast(q); - glm::mat4 S = glm::scale(glm::mat4(1.0f), scale); - modelMatrix = T * R * S; - matrixDirty = false; +void TransformComponent::UpdateModelMatrix() +{ + // Compose rotation with quaternions for stability and to avoid rad/deg ambiguity + glm::mat4 T = glm::translate(glm::mat4(1.0f), position); + glm::quat qx = glm::angleAxis(rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + glm::quat qy = glm::angleAxis(rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::quat qz = glm::angleAxis(rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); + glm::quat q = qz * qy * qx; // ZYX order is conventional for Euler composition + glm::mat4 R = glm::mat4_cast(q); + glm::mat4 S = glm::scale(glm::mat4(1.0f), scale); + modelMatrix = T * R * S; + matrixDirty = false; } diff --git a/attachments/simple_engine/transform_component.h b/attachments/simple_engine/transform_component.h index 72ef7ec0..fb73b937 100644 --- a/attachments/simple_engine/transform_component.h +++ b/attachments/simple_engine/transform_component.h @@ -1,3 +1,19 @@ +/* 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 @@ -12,119 +28,131 @@ * This class implements the transform system as described in the Camera_Transformations chapter: * @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix */ -class TransformComponent final : public Component { -private: - glm::vec3 position = {0.0f, 0.0f, 0.0f}; - glm::vec3 rotation = {0.0f, 0.0f, 0.0f}; // Euler angles in radians - glm::vec3 scale = {1.0f, 1.0f, 1.0f}; - - glm::mat4 modelMatrix = glm::mat4(1.0f); - bool matrixDirty = true; - -public: - /** - * @brief Constructor with an optional name. - * @param componentName The name of the component. - */ - explicit TransformComponent(const std::string& componentName = "TransformComponent") - : Component(componentName) {} - - /** - * @brief Set the position of the entity. - * @param newPosition The new position. - */ - void SetPosition(const glm::vec3& newPosition) { - position = newPosition; - matrixDirty = true; - } - - /** - * @brief Get the position of the entity. - * @return The position. - */ - const glm::vec3& GetPosition() const { - return position; - } - - /** - * @brief Set the rotation of the entity using Euler angles. - * @param newRotation The new rotation in radians. - */ - void SetRotation(const glm::vec3& newRotation) { - rotation = newRotation; - matrixDirty = true; - } - - /** - * @brief Get the rotation of the entity as Euler angles. - * @return The rotation in radians. - */ - const glm::vec3& GetRotation() const { - return rotation; - } - - /** - * @brief Set the scale of the entity. - * @param newScale The new scale. - */ - void SetScale(const glm::vec3& newScale) { - scale = newScale; - matrixDirty = true; - } - - /** - * @brief Get the scale of the entity. - * @return The scale. - */ - const glm::vec3& GetScale() const { - return scale; - } - - /** - * @brief Set the uniform scale of the entity. - * @param uniformScale The new uniform scale. - */ - void SetUniformScale(float uniformScale) { - scale = glm::vec3(uniformScale); - matrixDirty = true; - } - - /** - * @brief Translate the entity relative to its current position. - * @param translation The translation to apply. - */ - void Translate(const glm::vec3& translation) { - position += translation; - matrixDirty = true; - } - - /** - * @brief Rotate the entity relative to its current rotation. - * @param eulerAngles The rotation to apply in radians. - */ - void Rotate(const glm::vec3& eulerAngles) { - rotation += eulerAngles; - matrixDirty = true; - } - - /** - * @brief Scale the entity relative to its current scale. - * @param scaleFactors The scale factors to apply. - */ - void Scale(const glm::vec3& scaleFactors) { - scale *= scaleFactors; - matrixDirty = true; - } - - /** - * @brief Get the model matrix for this transform. - * @return The model matrix. - */ - const glm::mat4& GetModelMatrix(); - -private: - /** - * @brief Update the model matrix based on position, rotation, and scale. - */ - void UpdateModelMatrix(); +class TransformComponent final : public Component +{ + private: + glm::vec3 position = {0.0f, 0.0f, 0.0f}; + glm::vec3 rotation = {0.0f, 0.0f, 0.0f}; // Euler angles in radians + glm::vec3 scale = {1.0f, 1.0f, 1.0f}; + + glm::mat4 modelMatrix = glm::mat4(1.0f); + bool matrixDirty = true; + + public: + /** + * @brief Constructor with an optional name. + * @param componentName The name of the component. + */ + explicit TransformComponent(const std::string &componentName = "TransformComponent") : + Component(componentName) + {} + + /** + * @brief Set the position of the entity. + * @param newPosition The new position. + */ + void SetPosition(const glm::vec3 &newPosition) + { + position = newPosition; + matrixDirty = true; + } + + /** + * @brief Get the position of the entity. + * @return The position. + */ + const glm::vec3 &GetPosition() const + { + return position; + } + + /** + * @brief Set the rotation of the entity using Euler angles. + * @param newRotation The new rotation in radians. + */ + void SetRotation(const glm::vec3 &newRotation) + { + rotation = newRotation; + matrixDirty = true; + } + + /** + * @brief Get the rotation of the entity as Euler angles. + * @return The rotation in radians. + */ + const glm::vec3 &GetRotation() const + { + return rotation; + } + + /** + * @brief Set the scale of the entity. + * @param newScale The new scale. + */ + void SetScale(const glm::vec3 &newScale) + { + scale = newScale; + matrixDirty = true; + } + + /** + * @brief Get the scale of the entity. + * @return The scale. + */ + const glm::vec3 &GetScale() const + { + return scale; + } + + /** + * @brief Set the uniform scale of the entity. + * @param uniformScale The new uniform scale. + */ + void SetUniformScale(float uniformScale) + { + scale = glm::vec3(uniformScale); + matrixDirty = true; + } + + /** + * @brief Translate the entity relative to its current position. + * @param translation The translation to apply. + */ + void Translate(const glm::vec3 &translation) + { + position += translation; + matrixDirty = true; + } + + /** + * @brief Rotate the entity relative to its current rotation. + * @param eulerAngles The rotation to apply in radians. + */ + void Rotate(const glm::vec3 &eulerAngles) + { + rotation += eulerAngles; + matrixDirty = true; + } + + /** + * @brief Scale the entity relative to its current scale. + * @param scaleFactors The scale factors to apply. + */ + void Scale(const glm::vec3 &scaleFactors) + { + scale *= scaleFactors; + matrixDirty = true; + } + + /** + * @brief Get the model matrix for this transform. + * @return The model matrix. + */ + const glm::mat4 &GetModelMatrix(); + + private: + /** + * @brief Update the model matrix based on position, rotation, and scale. + */ + void UpdateModelMatrix(); }; diff --git a/attachments/simple_engine/vulkan_device.cpp b/attachments/simple_engine/vulkan_device.cpp index 5c10825b..b446ddf7 100644 --- a/attachments/simple_engine/vulkan_device.cpp +++ b/attachments/simple_engine/vulkan_device.cpp @@ -1,292 +1,329 @@ +/* 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 "vulkan_device.h" -#include -#include #include +#include #include +#include // Constructor -VulkanDevice::VulkanDevice(vk::raii::Instance& instance, vk::raii::SurfaceKHR& surface, - const std::vector& requiredExtensions, - const std::vector& optionalExtensions) - : instance(instance), surface(surface), - requiredExtensions(requiredExtensions), - optionalExtensions(optionalExtensions) { - - // Initialize deviceExtensions with required extensions - deviceExtensions = requiredExtensions; - - // Add optional extensions - deviceExtensions.insert(deviceExtensions.end(), optionalExtensions.begin(), optionalExtensions.end()); +VulkanDevice::VulkanDevice(vk::raii::Instance &instance, vk::raii::SurfaceKHR &surface, + const std::vector &requiredExtensions, + const std::vector &optionalExtensions) : + instance(instance), surface(surface), requiredExtensions(requiredExtensions), optionalExtensions(optionalExtensions) +{ + // Initialize deviceExtensions with required extensions + deviceExtensions = requiredExtensions; + + // Add optional extensions + deviceExtensions.insert(deviceExtensions.end(), optionalExtensions.begin(), optionalExtensions.end()); } // Destructor -VulkanDevice::~VulkanDevice() { - // RAII will handle destruction +VulkanDevice::~VulkanDevice() +{ + // RAII will handle destruction } // Pick physical device - improved implementation based on 37_multithreading.cpp -bool VulkanDevice::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; - } - - // Find a suitable device using modern C++ ranges - const auto devIter = std::ranges::find_if( - devices, - [&](auto& device) { - // Print device properties for debugging - vk::PhysicalDeviceProperties deviceProperties = device.getProperties(); - std::cout << "Checking device: " << deviceProperties.deviceName << std::endl; - - // Check if device supports Vulkan 1.3 - bool supportsVulkan1_3 = deviceProperties.apiVersion >= vk::ApiVersion13; - if (!supportsVulkan1_3) { - std::cout << " - Does not support Vulkan 1.3" << std::endl; - } - - // Check queue families - QueueFamilyIndices indices = findQueueFamilies(device); - bool supportsGraphics = indices.isComplete(); - if (!supportsGraphics) { - std::cout << " - Missing required queue families" << std::endl; - } - - // Check device extensions - bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(device); - if (!supportsAllRequiredExtensions) { - std::cout << " - Missing required extensions" << std::endl; - } - - // Check swap chain support - bool swapChainAdequate = false; - if (supportsAllRequiredExtensions) { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); - swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); - if (!swapChainAdequate) { - std::cout << " - Inadequate swap chain support" << std::endl; - } - } - - // Check for required features - auto features = device.template getFeatures2(); - bool supportsRequiredFeatures = features.template get().dynamicRendering; - if (!supportsRequiredFeatures) { - std::cout << " - Does not support required features (dynamicRendering)" << std::endl; - } - - supportsRequiredFeatures &= features.template get().attachmentFeedbackLoopLayout; - if (!supportsRequiredFeatures) { - std::cout << " - Does not support required feature (attachmentFeedbackLoopLayout)" << std::endl; - } - - return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; - }); - - if (devIter != devices.end()) { - physicalDevice = *devIter; - vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); - std::cout << "Selected device: " << deviceProperties.deviceName << std::endl; - - // Store queue family indices for the selected device - queueFamilyIndices = findQueueFamilies(physicalDevice); - return true; - } else { - 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; - } +bool VulkanDevice::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; + } + + // Find a suitable device using modern C++ ranges + const auto devIter = std::ranges::find_if( + devices, + [&](auto &device) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName << std::endl; + + // Check if device supports Vulkan 1.3 + bool supportsVulkan1_3 = deviceProperties.apiVersion >= vk::ApiVersion13; + if (!supportsVulkan1_3) + { + std::cout << " - Does not support Vulkan 1.3" << std::endl; + } + + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + bool supportsGraphics = indices.isComplete(); + if (!supportsGraphics) + { + std::cout << " - Missing required queue families" << std::endl; + } + + // Check device extensions + bool supportsAllRequiredExtensions = checkDeviceExtensionSupport(device); + if (!supportsAllRequiredExtensions) + { + std::cout << " - Missing required extensions" << std::endl; + } + + // Check swap chain support + bool swapChainAdequate = false; + if (supportsAllRequiredExtensions) + { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + if (!swapChainAdequate) + { + std::cout << " - Inadequate swap chain support" << std::endl; + } + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + if (!supportsRequiredFeatures) + { + std::cout << " - Does not support required features (dynamicRendering)" << std::endl; + } + + supportsRequiredFeatures &= features.template get().attachmentFeedbackLoopLayout; + if (!supportsRequiredFeatures) + { + std::cout << " - Does not support required feature (attachmentFeedbackLoopLayout)" << std::endl; + } + + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && swapChainAdequate && supportsRequiredFeatures; + }); + + if (devIter != devices.end()) + { + physicalDevice = *devIter; + vk::PhysicalDeviceProperties deviceProperties = physicalDevice.getProperties(); + std::cout << "Selected device: " << deviceProperties.deviceName << std::endl; + + // Store queue family indices for the selected device + queueFamilyIndices = findQueueFamilies(physicalDevice); + return true; + } + else + { + 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; + } } // Create logical device -bool VulkanDevice::createLogicalDevice(bool enableValidationLayers, const std::vector& validationLayers) { - try { - // Create queue create infos for each unique queue family - std::vector queueCreateInfos; - std::set uniqueQueueFamilies = { +bool VulkanDevice::createLogicalDevice(bool enableValidationLayers, const std::vector &validationLayers) +{ + try + { + // Create queue create infos for each unique queue family + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = { queueFamilyIndices.graphicsFamily.value(), queueFamilyIndices.presentFamily.value(), - queueFamilyIndices.computeFamily.value() - }; - - float queuePriority = 1.0f; - for (uint32_t queueFamily : uniqueQueueFamilies) { - vk::DeviceQueueCreateInfo queueCreateInfo{ - .queueFamilyIndex = queueFamily, - .queueCount = 1, - .pQueuePriorities = &queuePriority - }; - queueCreateInfos.push_back(queueCreateInfo); - } - - // Enable required features - auto features = physicalDevice.getFeatures2(); - features.features.samplerAnisotropy = vk::True; - features.features.depthClamp = vk::True; - - // Enable Vulkan 1.3 features - vk::PhysicalDeviceVulkan13Features vulkan13Features; - vulkan13Features.dynamicRendering = vk::True; - vulkan13Features.synchronization2 = vk::True; - features.pNext = &vulkan13Features; - - vk::PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT feedbackLoopFeatures{}; - feedbackLoopFeatures.attachmentFeedbackLoopLayout = vk::True; - - vulkan13Features.pNext = &feedbackLoopFeatures; - - // Create device. Device layers are deprecated and ignored, so we - // configure only extensions and features; 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); - - // 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); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to create logical device: " << e.what() << std::endl; - return false; - } + queueFamilyIndices.computeFamily.value()}; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) + { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority}; + queueCreateInfos.push_back(queueCreateInfo); + } + + // Enable required features + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = vk::True; + features.features.depthClamp = vk::True; + + // Enable Vulkan 1.3 features + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + features.pNext = &vulkan13Features; + + vk::PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT feedbackLoopFeatures{}; + feedbackLoopFeatures.attachmentFeedbackLoopLayout = vk::True; + + vulkan13Features.pNext = &feedbackLoopFeatures; + + // Create device. Device layers are deprecated and ignored, so we + // configure only extensions and features; 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); + + // 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); + + return true; + } + catch (const std::exception &e) + { + std::cerr << "Failed to create logical device: " << e.what() << std::endl; + return false; + } } // Find queue families -QueueFamilyIndices VulkanDevice::findQueueFamilies(vk::raii::PhysicalDevice& device) { - QueueFamilyIndices indices; - - // Get queue family properties - std::vector queueFamilies = device.getQueueFamilyProperties(); - - // Find queue families that support graphics, compute, and present - for (uint32_t i = 0; i < queueFamilies.size(); i++) { - // Check for graphics support - if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) { - indices.graphicsFamily = i; - } - - // Check for compute support - if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) { - indices.computeFamily = i; - } - - // Check for present support - if (device.getSurfaceSupportKHR(i, surface)) { - indices.presentFamily = i; - } - - // If all queue families are found, break - if (indices.isComplete()) { - break; - } - } - - return indices; +QueueFamilyIndices VulkanDevice::findQueueFamilies(vk::raii::PhysicalDevice &device) +{ + QueueFamilyIndices indices; + + // Get queue family properties + std::vector queueFamilies = device.getQueueFamilyProperties(); + + // Find queue families that support graphics, compute, and present + for (uint32_t i = 0; i < queueFamilies.size(); i++) + { + // Check for graphics support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) + { + indices.graphicsFamily = i; + } + + // Check for compute support + if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) + { + indices.computeFamily = i; + } + + // Check for present support + if (device.getSurfaceSupportKHR(i, surface)) + { + indices.presentFamily = i; + } + + // If all queue families are found, break + if (indices.isComplete()) + { + break; + } + } + + return indices; } // Query swap chain support -SwapChainSupportDetails VulkanDevice::querySwapChainSupport(vk::raii::PhysicalDevice& device) { - SwapChainSupportDetails details; +SwapChainSupportDetails VulkanDevice::querySwapChainSupport(vk::raii::PhysicalDevice &device) +{ + SwapChainSupportDetails details; - // Get surface capabilities - details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); - // Get surface formats - details.formats = device.getSurfaceFormatsKHR(surface); + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); - // Get present modes - details.presentModes = device.getSurfacePresentModesKHR(surface); + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); - return details; + return details; } // Check device extension support -bool VulkanDevice::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { - // Get available extensions - std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); - - // Only check for required extensions, not optional ones - std::set requiredExtensionsSet(requiredExtensions.begin(), requiredExtensions.end()); - - // Print available extensions for debugging - std::cout << "Available extensions:" << std::endl; - for (const auto& extension : availableExtensions) { - std::cout << " " << extension.extensionName << std::endl; - 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; - } - - // Check which optional extensions are supported - std::set optionalExtensionsSet(optionalExtensions.begin(), optionalExtensions.end()); - std::cout << "Supported optional extensions:" << std::endl; - for (const auto& extension : availableExtensions) { - if (optionalExtensionsSet.contains(extension.extensionName)) { - std::cout << " " << extension.extensionName << " (supported)" << std::endl; - } - } - - return true; +bool VulkanDevice::checkDeviceExtensionSupport(vk::raii::PhysicalDevice &device) +{ + // Get available extensions + std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); + + // Only check for required extensions, not optional ones + std::set requiredExtensionsSet(requiredExtensions.begin(), requiredExtensions.end()); + + for (const auto &extension : availableExtensions) + { + 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 a device is suitable -bool VulkanDevice::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; +bool VulkanDevice::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; } // Find memory type -uint32_t VulkanDevice::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { - // 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"); +uint32_t VulkanDevice::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const +{ + // 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"); } diff --git a/attachments/simple_engine/vulkan_device.h b/attachments/simple_engine/vulkan_device.h index 8d5b55db..51b5bcb1 100644 --- a/attachments/simple_engine/vulkan_device.h +++ b/attachments/simple_engine/vulkan_device.h @@ -1,148 +1,186 @@ +/* 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 /** * @brief Structure for Vulkan queue family indices. */ -struct QueueFamilyIndices { - std::optional graphicsFamily; - std::optional presentFamily; - std::optional computeFamily; - - bool isComplete() const { - return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); - } +struct QueueFamilyIndices +{ + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + + 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; +struct SwapChainSupportDetails +{ + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; }; /** * @brief Class for managing Vulkan device selection and creation. */ -class VulkanDevice { -public: - /** - * @brief Constructor. - * @param instance The Vulkan instance. - * @param surface The Vulkan surface. - * @param requiredExtensions The required device extensions. - * @param optionalExtensions The optional device extensions. - */ - VulkanDevice(vk::raii::Instance& instance, vk::raii::SurfaceKHR& surface, - const std::vector& requiredExtensions, - const std::vector& optionalExtensions = {}); - - /** - * @brief Destructor. - */ - ~VulkanDevice(); - - /** - * @brief Pick a suitable physical device. - * @return True if a suitable device was found, false otherwise. - */ - bool pickPhysicalDevice(); - - /** - * @brief Create a logical device. - * @param enableValidationLayers Whether to enable validation layers. - * @param validationLayers The validation layers to enable. - * @return True if the logical device was created successfully, false otherwise. - */ - bool createLogicalDevice(bool enableValidationLayers, const std::vector& validationLayers); - - /** - * @brief Get the physical device. - * @return The physical device. - */ - vk::raii::PhysicalDevice& getPhysicalDevice() { return physicalDevice; } - - /** - * @brief Get the logical device. - * @return The logical device. - */ - vk::raii::Device& getDevice() { return device; } - - /** - * @brief Get the graphics queue. - * @return The graphics queue. - */ - vk::raii::Queue& getGraphicsQueue() { return graphicsQueue; } - - /** - * @brief Get the present queue. - * @return The present queue. - */ - vk::raii::Queue& getPresentQueue() { return presentQueue; } - - /** - * @brief Get the compute queue. - * @return The compute queue. - */ - vk::raii::Queue& getComputeQueue() { return computeQueue; } - - /** - * @brief Get the queue family indices. - * @return The queue family indices. - */ - QueueFamilyIndices getQueueFamilyIndices() const { return queueFamilyIndices; } - - /** - * @brief Find queue families for a physical device. - * @param device The physical device. - * @return The queue family indices. - */ - QueueFamilyIndices findQueueFamilies(vk::raii::PhysicalDevice& device); - - /** - * @brief Query swap chain support for a physical device. - * @param device The physical device. - * @return The swap chain support details. - */ - SwapChainSupportDetails querySwapChainSupport(vk::raii::PhysicalDevice& device); - - /** - * @brief Find a memory type with the specified properties. - * @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; - -private: - // Vulkan instance and surface - vk::raii::Instance& instance; - vk::raii::SurfaceKHR& surface; - - // Vulkan device - vk::raii::PhysicalDevice physicalDevice = nullptr; - vk::raii::Device device = nullptr; - - // Vulkan queues - vk::raii::Queue graphicsQueue = nullptr; - vk::raii::Queue presentQueue = nullptr; - vk::raii::Queue computeQueue = nullptr; - - // Queue family indices - QueueFamilyIndices queueFamilyIndices; - - // Device extensions - std::vector requiredExtensions; - std::vector optionalExtensions; - std::vector deviceExtensions; - - // Private methods - bool isDeviceSuitable(vk::raii::PhysicalDevice& device); - bool checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device); +class VulkanDevice +{ + public: + /** + * @brief Constructor. + * @param instance The Vulkan instance. + * @param surface The Vulkan surface. + * @param requiredExtensions The required device extensions. + * @param optionalExtensions The optional device extensions. + */ + VulkanDevice(vk::raii::Instance &instance, vk::raii::SurfaceKHR &surface, + const std::vector &requiredExtensions, + const std::vector &optionalExtensions = {}); + + /** + * @brief Destructor. + */ + ~VulkanDevice(); + + /** + * @brief Pick a suitable physical device. + * @return True if a suitable device was found, false otherwise. + */ + bool pickPhysicalDevice(); + + /** + * @brief Create a logical device. + * @param enableValidationLayers Whether to enable validation layers. + * @param validationLayers The validation layers to enable. + * @return True if the logical device was created successfully, false otherwise. + */ + bool createLogicalDevice(bool enableValidationLayers, const std::vector &validationLayers); + + /** + * @brief Get the physical device. + * @return The physical device. + */ + vk::raii::PhysicalDevice &getPhysicalDevice() + { + return physicalDevice; + } + + /** + * @brief Get the logical device. + * @return The logical device. + */ + vk::raii::Device &getDevice() + { + return device; + } + + /** + * @brief Get the graphics queue. + * @return The graphics queue. + */ + vk::raii::Queue &getGraphicsQueue() + { + return graphicsQueue; + } + + /** + * @brief Get the present queue. + * @return The present queue. + */ + vk::raii::Queue &getPresentQueue() + { + return presentQueue; + } + + /** + * @brief Get the compute queue. + * @return The compute queue. + */ + vk::raii::Queue &getComputeQueue() + { + return computeQueue; + } + + /** + * @brief Get the queue family indices. + * @return The queue family indices. + */ + QueueFamilyIndices getQueueFamilyIndices() const + { + return queueFamilyIndices; + } + + /** + * @brief Find queue families for a physical device. + * @param device The physical device. + * @return The queue family indices. + */ + QueueFamilyIndices findQueueFamilies(vk::raii::PhysicalDevice &device); + + /** + * @brief Query swap chain support for a physical device. + * @param device The physical device. + * @return The swap chain support details. + */ + SwapChainSupportDetails querySwapChainSupport(vk::raii::PhysicalDevice &device); + + /** + * @brief Find a memory type with the specified properties. + * @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; + + private: + // Vulkan instance and surface + vk::raii::Instance &instance; + vk::raii::SurfaceKHR &surface; + + // Vulkan device + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + + // Vulkan queues + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::Queue presentQueue = nullptr; + vk::raii::Queue computeQueue = nullptr; + + // Queue family indices + QueueFamilyIndices queueFamilyIndices; + + // Device extensions + std::vector requiredExtensions; + std::vector optionalExtensions; + std::vector deviceExtensions; + + // Private methods + bool isDeviceSuitable(vk::raii::PhysicalDevice &device); + bool checkDeviceExtensionSupport(vk::raii::PhysicalDevice &device); }; diff --git a/attachments/simple_engine/vulkan_dispatch.cpp b/attachments/simple_engine/vulkan_dispatch.cpp index daa4a73e..2536dbf1 100644 --- a/attachments/simple_engine/vulkan_dispatch.cpp +++ b/attachments/simple_engine/vulkan_dispatch.cpp @@ -1,6 +1,23 @@ +/* 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 // Define the defaultDispatchLoaderDynamic variable -namespace vk::detail { - DispatchLoaderDynamic defaultDispatchLoaderDynamic; +namespace vk::detail +{ +DispatchLoaderDynamic defaultDispatchLoaderDynamic; } From 37cb78e9605febe2d8e0ac856efaadd2643b7ef2 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 16 Dec 2025 03:16:24 -0800 Subject: [PATCH 02/24] Remove unused debug comments and code fragments from rendering logic and shaders for cleaner production-ready code. --- attachments/simple_engine/renderer_rendering.cpp | 5 ----- attachments/simple_engine/shaders/pbr.slang | 9 +-------- attachments/simple_engine/shaders/ray_query.slang | 15 --------------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 45fefbf5..c0e610d1 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -312,7 +312,6 @@ void Renderer::renderReflectionPass(vk::raii::CommandBuffer & CameraComponent *camera, const std::vector> &entities) { - // Initial scaffolding: clear the reflection RT; drawing the mirrored scene will be added next. if (reflections.empty()) return; auto &rt = reflections[currentFrame]; @@ -2395,10 +2394,7 @@ void Renderer::Render(const std::vector> &entities, Came // As a last guard before dispatch, make sure compute binding 0 is valid for this frame refreshForwardPlusComputeLightsBindingForFrame(currentFrame); - // Forward+ per-frame debug printing removed - dispatchForwardPlus(commandBuffers[currentFrame], tilesX, tilesY, forwardPlusSlicesZ); - // Forward+ debug dumps and tile header prints removed } // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE @@ -2418,7 +2414,6 @@ void Renderer::Render(const std::vector> &entities, Came // Clear the off-screen target at the start of opaque rendering to a neutral black background vk::RenderingAttachmentInfo colorAttachment{.imageView = *opaqueSceneColorImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})}; depthAttachment.imageView = *depthImageView; - // Load depth only if we actually performed a pre-pass (and not in opaque-only debug which intentionally ignores transparency ordering) 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); diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 7b67e9f4..8d2b6adf 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -190,8 +190,6 @@ float4 PSMain(VSOutput input) : SV_TARGET bool forceGlobal = false; - // No one-frame fragment debug - // Accumulate per-light diffuse and specular terms using GGX microfacet BRDF. if (!forceGlobal && count > 0) { // Use Forward+ culled list @@ -235,7 +233,6 @@ float4 PSMain(VSOutput input) : SV_TARGET float3 kD = (1.0 - F) * (1.0 - metallic); specularLighting += spec * radiance * NdotL; diffuseLighting += (kD * albedo / PI) * radiance * NdotL; - // no debug writes } } } @@ -279,7 +276,6 @@ float4 PSMain(VSOutput input) : SV_TARGET } } - // No debug paths float3 ambient = albedo * ao * (0.03 * ubo.scaleIBLAmbient); float3 opaqueLit = diffuseLighting + specularLighting + ambient + emissive; @@ -392,7 +388,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET refl = opaqueSceneColor.Sample(uvR).rgb; } - // Stylized, stable glass: do NOT sample opaqueSceneColor. Use a tinted + // 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 @@ -522,7 +518,6 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET // --- 3. Post-processing (same as PSMain) --- - // No debug recording color *= ubo.exposure; // Uncharted2 / Hable filmic tonemap. Use the canonical form without @@ -538,7 +533,5 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET color = saturate(color); } - // (fragment debug disabled in this build) - return float4(color, alphaOut); } \ No newline at end of file diff --git a/attachments/simple_engine/shaders/ray_query.slang b/attachments/simple_engine/shaders/ray_query.slang index f99719ff..f43406fe 100644 --- a/attachments/simple_engine/shaders/ray_query.slang +++ b/attachments/simple_engine/shaders/ray_query.slang @@ -472,8 +472,6 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { } } - // (No debugPrintf in production) - // Check if we hit anything if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { result.hit = true; @@ -493,8 +491,6 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { uint blasIndex = instIndex; uint primitiveIndex = q.CommittedPrimitiveIndex(); - // Minimal debug disabled in production - // Validate instance index is in bounds of geometry info buffer if (blasIndex >= uint(max(0, ubo.geometryInfoCount))) { // Invalid BLAS index, use default values @@ -575,8 +571,6 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { uint i1 = indexBuffer[idxBase + 1u]; uint i2 = indexBuffer[idxBase + 2u]; - // CRITICAL: Validate vertex indices are within bounds to prevent GPU hang - // Reading beyond vertex buffer bounds causes GPU fault/hang 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); @@ -780,8 +774,6 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { result.alphaMode = material.alphaMode; result.opacity = clamp(material.alpha, 0.0, 1.0); result.thinWalled = (material.thinWalled != 0); - - // (No debug-only overrides in production) } return result; @@ -800,8 +792,6 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) return; } - // (No debug-only heartbeat/screen-test paths in production) - // Generate primary ray float2 uv = (float2(pixelCoord) + 0.5) / float2(imageDim); float2 ndc = uv * 2.0 - 1.0; @@ -832,18 +822,13 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) HitInfo hit = traceRay(rayOrigin, rayDir, 0.0001, 10000.0); - // Production: normal shading (no instance visualization) - bool colorByInstance = false; - if (hit.hit) { float3 c = shadeWithSecondaryRays(rayOrigin, rayDir, hit); // Output linear HDR-ish color; composite pass will apply exposure/gamma. outputImage[pixelCoord] = float4(c, 1.0); - // (Debug print hooks intentionally removed from default path) } else { // Sky/background color outputImage[pixelCoord] = float4(skyColor(rayDir), 1.0); } - // Debug probes disabled in production } From 2176e06449ade368645de4d13d4a4f27dfac578b Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 16 Dec 2025 14:08:22 -0800 Subject: [PATCH 03/24] Add CMake find scripts for nlohmann_json, tinygltf, VulkanHpp, Vulkan-Profiles, and GLM libraries, enabling automated detection and integration of dependencies. --- attachments/simple_engine/CMake/FindKTX.cmake | 106 ++ .../simple_engine/CMake/FindVulkan.cmake | 937 ++++++++++++++++++ .../simple_engine/CMake/FindVulkanHpp.cmake | 426 ++++++++ attachments/simple_engine/CMake/Findglm.cmake | 133 +++ .../CMake/Findnlohmann_json.cmake | 154 +++ attachments/simple_engine/CMake/Findstb.cmake | 86 ++ .../simple_engine/CMake/Findtinygltf.cmake | 162 +++ .../CMake/Findtinyobjloader.cmake | 160 +++ 8 files changed, 2164 insertions(+) create mode 100644 attachments/simple_engine/CMake/FindKTX.cmake create mode 100644 attachments/simple_engine/CMake/FindVulkan.cmake create mode 100644 attachments/simple_engine/CMake/FindVulkanHpp.cmake create mode 100644 attachments/simple_engine/CMake/Findglm.cmake create mode 100644 attachments/simple_engine/CMake/Findnlohmann_json.cmake create mode 100644 attachments/simple_engine/CMake/Findstb.cmake create mode 100644 attachments/simple_engine/CMake/Findtinygltf.cmake create mode 100644 attachments/simple_engine/CMake/Findtinyobjloader.cmake diff --git a/attachments/simple_engine/CMake/FindKTX.cmake b/attachments/simple_engine/CMake/FindKTX.cmake new file mode 100644 index 00000000..ac6971a2 --- /dev/null +++ b/attachments/simple_engine/CMake/FindKTX.cmake @@ -0,0 +1,106 @@ +# FindKTX.cmake +# +# Finds the KTX library +# +# This will define the following variables +# +# KTX_FOUND +# KTX_INCLUDE_DIRS +# KTX_LIBRARIES +# +# and the following imported targets +# +# KTX::ktx +# + +# Check if we're on Linux - if so, we'll skip the search and directly use FetchContent +if(UNIX AND NOT APPLE) + # On Linux, we assume KTX is not installed and proceed directly to fetching it + set(KTX_FOUND FALSE) +else() + # On non-Linux platforms, try to find KTX using pkg-config first + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_KTX QUIET ktx libktx ktx2 libktx2) + endif() + + # Try to find KTX using standard find_package + find_path(KTX_INCLUDE_DIR + NAMES ktx.h + PATH_SUFFIXES include ktx KTX ktx2 KTX2 + HINTS + ${PC_KTX_INCLUDEDIR} + /usr/include + /usr/local/include + $ENV{KTX_DIR}/include + $ENV{VULKAN_SDK}/include + ${CMAKE_SOURCE_DIR}/external/ktx/include + ) + + find_library(KTX_LIBRARY + NAMES ktx ktx2 libktx libktx2 + PATH_SUFFIXES lib lib64 + HINTS + ${PC_KTX_LIBDIR} + /usr/lib + /usr/lib64 + /usr/local/lib + /usr/local/lib64 + $ENV{KTX_DIR}/lib + $ENV{VULKAN_SDK}/lib + ${CMAKE_SOURCE_DIR}/external/ktx/lib + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(KTX + REQUIRED_VARS KTX_INCLUDE_DIR KTX_LIBRARY + FAIL_MESSAGE "" # Suppress the error message to allow our fallback + ) + + # Debug output if KTX is not found (only on non-Linux platforms) + if(NOT KTX_FOUND) + message(STATUS "KTX include directory search paths: ${PC_KTX_INCLUDEDIR}, /usr/include, /usr/local/include, $ENV{KTX_DIR}/include, $ENV{VULKAN_SDK}/include, ${CMAKE_SOURCE_DIR}/external/ktx/include") + message(STATUS "KTX library search paths: ${PC_KTX_LIBDIR}, /usr/lib, /usr/lib64, /usr/local/lib, /usr/local/lib64, $ENV{KTX_DIR}/lib, $ENV{VULKAN_SDK}/lib, ${CMAKE_SOURCE_DIR}/external/ktx/lib") + endif() +endif() + +if(KTX_FOUND) + set(KTX_INCLUDE_DIRS ${KTX_INCLUDE_DIR}) + set(KTX_LIBRARIES ${KTX_LIBRARY}) + + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx UNKNOWN IMPORTED) + set_target_properties(KTX::ktx PROPERTIES + IMPORTED_LOCATION "${KTX_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${KTX_INCLUDE_DIRS}" + ) + endif() +else() + # If not found, use FetchContent to download and build + include(FetchContent) + + # Only show the message on non-Linux platforms + if(NOT (UNIX AND NOT APPLE)) + message(STATUS "KTX not found, fetching from GitHub...") + endif() + + FetchContent_Declare( + ktx + GIT_REPOSITORY https://github.com/KhronosGroup/KTX-Software.git + GIT_TAG v4.3.1 # Use a specific tag for stability + ) + + # Set options to minimize build time and dependencies + set(KTX_FEATURE_TOOLS OFF CACHE BOOL "Build KTX tools" FORCE) + set(KTX_FEATURE_DOC OFF CACHE BOOL "Build KTX documentation" FORCE) + set(KTX_FEATURE_TESTS OFF CACHE BOOL "Build KTX tests" FORCE) + + FetchContent_MakeAvailable(ktx) + + # Create an alias to match the expected target name + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx ALIAS ktx) + endif() + + set(KTX_FOUND TRUE) +endif() diff --git a/attachments/simple_engine/CMake/FindVulkan.cmake b/attachments/simple_engine/CMake/FindVulkan.cmake new file mode 100644 index 00000000..55ac153e --- /dev/null +++ b/attachments/simple_engine/CMake/FindVulkan.cmake @@ -0,0 +1,937 @@ +# Updates for iOS Copyright (c) 2024, Holochip Inc. +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +FindVulkan +---------- + +.. versionadded:: 3.7 + +Find Vulkan, which is a low-overhead, cross-platform 3D graphics +and computing API. + +Optional COMPONENTS +^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.24 + +This module respects several optional COMPONENTS. +There are corresponding imported targets for each of these. + +``glslc`` + The SPIR-V compiler. + +``glslangValidator`` + The ``glslangValidator`` tool. + +``glslang`` + The SPIR-V generator library. + +``shaderc_combined`` + The static library for Vulkan shader compilation. + +``SPIRV-Tools`` + Tools to process SPIR-V modules. + +``MoltenVK`` + On macOS, an additional component ``MoltenVK`` is available. + +``dxc`` + .. versionadded:: 3.25 + + The DirectX Shader Compiler. + +The ``glslc`` and ``glslangValidator`` components are provided even +if not explicitly requested (for backward compatibility). + +IMPORTED Targets +^^^^^^^^^^^^^^^^ + +This module defines :prop_tgt:`IMPORTED` targets if Vulkan has been found: + +``Vulkan::Vulkan`` + The main Vulkan library. + +``Vulkan::glslc`` + .. versionadded:: 3.19 + + The GLSLC SPIR-V compiler, if it has been found. + +``Vulkan::Headers`` + .. versionadded:: 3.21 + + Provides just Vulkan headers include paths, if found. No library is + included in this target. This can be useful for applications that + load Vulkan library dynamically. + +``Vulkan::glslangValidator`` + .. versionadded:: 3.21 + + The glslangValidator tool, if found. It is used to compile GLSL and + HLSL shaders into SPIR-V. + +``Vulkan::glslang`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos-reference front-end shader parser and SPIR-V + generator library (glslang). + +``Vulkan::shaderc_combined`` + .. versionadded:: 3.24 + + Defined if SDK has the Google static library for Vulkan shader compilation + (shaderc_combined). + +``Vulkan::SPIRV-Tools`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos library to process SPIR-V modules + (SPIRV-Tools). + +``Vulkan::MoltenVK`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos library which implement a subset of Vulkan API + over Apple Metal graphics framework. (MoltenVK). + +``Vulkan::volk`` + .. versionadded:: 3.25 + + Defined if SDK has the Vulkan meta-loader (volk). + +``Vulkan::dxc_lib`` + .. versionadded:: 3.25 + + Defined if SDK has the DirectX shader compiler library. + +``Vulkan::dxc_exe`` + .. versionadded:: 3.25 + + Defined if SDK has the DirectX shader compiler CLI tool. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module defines the following variables: + +``Vulkan_FOUND`` + set to true if Vulkan was found +``Vulkan_INCLUDE_DIRS`` + include directories for Vulkan +``Vulkan_LIBRARIES`` + link against this library to use Vulkan +``Vulkan_VERSION`` + .. versionadded:: 3.23 + + value from ``vulkan/vulkan_core.h`` +``Vulkan_glslc_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslc executable. +``Vulkan_glslangValidator_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslangValidator executable. +``Vulkan_glslang_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslang library. +``Vulkan_shaderc_combined_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the shaderc_combined library. +``Vulkan_SPIRV-Tools_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the SPIRV-Tools library. +``Vulkan_MoltenVK_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the MoltenVK library. +``Vulkan_volk_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the volk library. + +``Vulkan_dxc_lib_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the DirectX shader compiler library. + +``Vulkan_dxc_exe_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the DirectX shader compiler CLI tool. + + +The module will also defines these cache variables: + +``Vulkan_INCLUDE_DIR`` + the Vulkan include directory +``Vulkan_LIBRARY`` + the path to the Vulkan library +``Vulkan_GLSLC_EXECUTABLE`` + the path to the GLSL SPIR-V compiler +``Vulkan_GLSLANG_VALIDATOR_EXECUTABLE`` + the path to the glslangValidator tool +``Vulkan_glslang_LIBRARY`` + .. versionadded:: 3.24 + + Path to the glslang library. +``Vulkan_shaderc_combined_LIBRARY`` + .. versionadded:: 3.24 + + Path to the shaderc_combined library. +``Vulkan_SPIRV-Tools_LIBRARY`` + .. versionadded:: 3.24 + + Path to the SPIRV-Tools library. +``Vulkan_MoltenVK_LIBRARY`` + .. versionadded:: 3.24 + + Path to the MoltenVK library. + +``Vulkan_volk_LIBRARY`` + .. versionadded:: 3.25 + + Path to the volk library. + +``Vulkan_dxc_LIBRARY`` + .. versionadded:: 3.25 + + Path to the DirectX shader compiler library. + +``Vulkan_dxc_EXECUTABLE`` + .. versionadded:: 3.25 + + Path to the DirectX shader compiler CLI tool. + +Hints +^^^^^ + +.. versionadded:: 3.18 + +The ``VULKAN_SDK`` environment variable optionally specifies the +location of the Vulkan SDK root directory for the given +architecture. It is typically set by sourcing the toplevel +``setup-env.sh`` script of the Vulkan SDK directory into the shell +environment. + +#]=======================================================================] + +cmake_policy(PUSH) +cmake_policy(SET CMP0057 NEW) + +# Provide compatibility with a common invalid component request that +# was silently ignored prior to CMake 3.24. +if("FATAL_ERROR" IN_LIST Vulkan_FIND_COMPONENTS) + message(AUTHOR_WARNING + "Ignoring unknown component 'FATAL_ERROR'.\n" + "The find_package() command documents no such argument." + ) + list(REMOVE_ITEM Vulkan_FIND_COMPONENTS "FATAL_ERROR") +endif() + +if(IOS) + get_filename_component(Vulkan_Target_SDK "$ENV{VULKAN_SDK}/.." REALPATH) + list(APPEND CMAKE_FRAMEWORK_PATH "${Vulkan_Target_SDK}/iOS/lib") + set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) + set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) + set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) +endif() + +# For backward compatibility as `FindVulkan` in previous CMake versions allow to retrieve `glslc` +# and `glslangValidator` without requesting the corresponding component. +if(NOT glslc IN_LIST Vulkan_FIND_COMPONENTS) + list(APPEND Vulkan_FIND_COMPONENTS glslc) +endif() +if(NOT glslangValidator IN_LIST Vulkan_FIND_COMPONENTS) + list(APPEND Vulkan_FIND_COMPONENTS glslangValidator) +endif() + +if(WIN32) + set(_Vulkan_library_name vulkan-1) + set(_Vulkan_hint_include_search_paths + "$ENV{VULKAN_SDK}/include" + ) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib" + "$ENV{VULKAN_SDK}/bin" + ) + else() + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin32" + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib32" + "$ENV{VULKAN_SDK}/bin32" + "$ENV{VULKAN_SDK}/lib" + "$ENV{VULKAN_SDK}/bin" + ) + endif() +else() + set(_Vulkan_library_name vulkan) + set(_Vulkan_hint_include_search_paths + "$ENV{VULKAN_SDK}/include" + ) + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib" + ) +endif() +if(APPLE AND DEFINED ENV{VULKAN_SDK}) + list(APPEND _Vulkan_hint_include_search_paths + "${Vulkan_Target_SDK}/macOS/include" + ) + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}/iOS/lib" + ) + elseif(CMAKE_SYSTEM_NAME STREQUAL "tvOS") + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}/tvOS/lib" + ) + else() + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}}/lib" + ) + endif() +endif() + +find_path(Vulkan_INCLUDE_DIR + NAMES vulkan/vulkan.h + HINTS + ${_Vulkan_hint_include_search_paths} +) +message(STATUS "vulkan_include_dir ${Vulkan_INCLUDE_DIR} search paths ${_Vulkan_hint_include_search_paths}") +mark_as_advanced(Vulkan_INCLUDE_DIR) + +find_library(Vulkan_LIBRARY + NAMES ${_Vulkan_library_name} + HINTS + ${_Vulkan_hint_library_search_paths} +) +message(STATUS "vulkan_library ${Vulkan_LIBRARY} search paths ${_Vulkan_hint_library_search_paths}") +mark_as_advanced(Vulkan_LIBRARY) + +find_library(Vulkan_Layer_API_DUMP + NAMES VkLayer_api_dump + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_API_DUMP) + +find_library(Vulkan_Layer_SHADER_OBJECT + NAMES VkLayer_khronos_shader_object + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(VkLayer_khronos_shader_object) + +find_library(Vulkan_Layer_SYNC2 + NAMES VkLayer_khronos_synchronization2 + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_SYNC2) + +find_library(Vulkan_Layer_VALIDATION + NAMES VkLayer_khronos_validation + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_VALIDATION) + +if(glslc IN_LIST Vulkan_FIND_COMPONENTS) + find_program(Vulkan_GLSLC_EXECUTABLE + NAMES glslc + HINTS + ${_Vulkan_hint_executable_search_paths} + ) + mark_as_advanced(Vulkan_GLSLC_EXECUTABLE) +endif() +if(glslangValidator IN_LIST Vulkan_FIND_COMPONENTS) + find_program(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE + NAMES glslangValidator + HINTS + ${_Vulkan_hint_executable_search_paths} + ) + mark_as_advanced(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE) +endif() +if(glslang IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_glslang-spirv_LIBRARY + NAMES SPIRV + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-spirv_LIBRARY) + + find_library(Vulkan_glslang-spirv_DEBUG_LIBRARY + NAMES SPIRVd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-spirv_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-oglcompiler_LIBRARY + NAMES OGLCompiler + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-oglcompiler_LIBRARY) + + find_library(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY + NAMES OGLCompilerd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-osdependent_LIBRARY + NAMES OSDependent + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-osdependent_LIBRARY) + + find_library(Vulkan_glslang-osdependent_DEBUG_LIBRARY + NAMES OSDependentd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-osdependent_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-machineindependent_LIBRARY + NAMES MachineIndependent + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-machineindependent_LIBRARY) + + find_library(Vulkan_glslang-machineindependent_DEBUG_LIBRARY + NAMES MachineIndependentd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-machineindependent_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-genericcodegen_LIBRARY + NAMES GenericCodeGen + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-genericcodegen_LIBRARY) + + find_library(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY + NAMES GenericCodeGend + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) + + find_library(Vulkan_glslang_LIBRARY + NAMES glslang + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang_LIBRARY) + + find_library(Vulkan_glslang_DEBUG_LIBRARY + NAMES glslangd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang_DEBUG_LIBRARY) +endif() +if(shaderc_combined IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_shaderc_combined_LIBRARY + NAMES shaderc_combined + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_shaderc_combined_LIBRARY) + + find_library(Vulkan_shaderc_combined_DEBUG_LIBRARY + NAMES shaderc_combinedd + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_shaderc_combined_DEBUG_LIBRARY) +endif() +if(SPIRV-Tools IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_SPIRV-Tools_LIBRARY + NAMES SPIRV-Tools + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_SPIRV-Tools_LIBRARY) + + find_library(Vulkan_SPIRV-Tools_DEBUG_LIBRARY + NAMES SPIRV-Toolsd + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_SPIRV-Tools_DEBUG_LIBRARY) +endif() +if(MoltenVK IN_LIST Vulkan_FIND_COMPONENTS) + # CMake has a bug in 3.28 that doesn't handle xcframeworks. Do it by hand for now. + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + if(CMAKE_VERSION VERSION_LESS 3.29) + set( _Vulkan_hint_library_search_paths ${Vulkan_Target_SDK}/ios/lib/MoltenVK.xcframework/ios-arm64) + else () + set( _Vulkan_hint_library_search_paths ${Vulkan_Target_SDK}/ios/lib/) + endif () + endif () + find_library(Vulkan_MoltenVK_LIBRARY + NAMES MoltenVK + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_MoltenVK_LIBRARY) + + find_path(Vulkan_MoltenVK_INCLUDE_DIR + NAMES MoltenVK/mvk_vulkan.h + HINTS + ${_Vulkan_hint_include_search_paths} + ) + mark_as_advanced(Vulkan_MoltenVK_INCLUDE_DIR) +endif() +if(volk IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_volk_LIBRARY + NAMES volk + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_Volk_LIBRARY) +endif() + +if (dxc IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_dxc_LIBRARY + NAMES dxcompiler + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_dxc_LIBRARY) + + find_program(Vulkan_dxc_EXECUTABLE + NAMES dxc + HINTS + ${_Vulkan_hint_executable_search_paths}) + mark_as_advanced(Vulkan_dxc_EXECUTABLE) +endif() + +if(Vulkan_GLSLC_EXECUTABLE) + set(Vulkan_glslc_FOUND TRUE) +else() + set(Vulkan_glslc_FOUND FALSE) +endif() + +if(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE) + set(Vulkan_glslangValidator_FOUND TRUE) +else() + set(Vulkan_glslangValidator_FOUND FALSE) +endif() + +if (Vulkan_dxc_EXECUTABLE) + set(Vulkan_dxc_exe_FOUND TRUE) +else() + set(Vulkan_dxc_exe_FOUND FALSE) +endif() + +function(_Vulkan_set_library_component_found component) + cmake_parse_arguments(PARSE_ARGV 1 _ARG + "NO_WARNING" + "" + "DEPENDENT_COMPONENTS") + + set(all_dependent_component_found TRUE) + foreach(dependent_component IN LISTS _ARG_DEPENDENT_COMPONENTS) + if(NOT Vulkan_${dependent_component}_FOUND) + set(all_dependent_component_found FALSE) + break() + endif() + endforeach() + + if(all_dependent_component_found AND (Vulkan_${component}_LIBRARY OR Vulkan_${component}_DEBUG_LIBRARY)) + set(Vulkan_${component}_FOUND TRUE PARENT_SCOPE) + + # For Windows Vulkan SDK, third party tools binaries are provided with different MSVC ABI: + # - Release binaries uses a runtime library + # - Debug binaries uses a debug runtime library + # This lead to incompatibilities in linking for some configuration types due to CMake-default or project-configured selected MSVC ABI. + if(WIN32 AND NOT _ARG_NO_WARNING) + if(NOT Vulkan_${component}_LIBRARY) + message(WARNING + "Library ${component} for Release configuration is missing, imported target Vulkan::${component} may not be able to link when targeting this build configuration due to incompatible MSVC ABI.") + endif() + if(NOT Vulkan_${component}_DEBUG_LIBRARY) + message(WARNING + "Library ${component} for Debug configuration is missing, imported target Vulkan::${component} may not be able to link when targeting this build configuration due to incompatible MSVC ABI. Consider re-installing the Vulkan SDK and request debug libraries to fix this warning.") + endif() + endif() + else() + set(Vulkan_${component}_FOUND FALSE PARENT_SCOPE) + endif() +endfunction() + +_Vulkan_set_library_component_found(glslang-spirv NO_WARNING) +_Vulkan_set_library_component_found(glslang-oglcompiler NO_WARNING) +_Vulkan_set_library_component_found(glslang-osdependent NO_WARNING) +_Vulkan_set_library_component_found(glslang-machineindependent NO_WARNING) +_Vulkan_set_library_component_found(glslang-genericcodegen NO_WARNING) +_Vulkan_set_library_component_found(glslang + DEPENDENT_COMPONENTS + glslang-spirv + glslang-oglcompiler + glslang-osdependent + glslang-machineindependent + glslang-genericcodegen) +_Vulkan_set_library_component_found(shaderc_combined) +_Vulkan_set_library_component_found(SPIRV-Tools) +_Vulkan_set_library_component_found(volk) +_Vulkan_set_library_component_found(dxc) + +if(Vulkan_MoltenVK_INCLUDE_DIR AND Vulkan_MoltenVK_LIBRARY) + set(Vulkan_MoltenVK_FOUND TRUE) +else() + set(Vulkan_MoltenVK_FOUND FALSE) +endif() + +set(Vulkan_LIBRARIES ${Vulkan_LIBRARY}) +set(Vulkan_INCLUDE_DIRS ${Vulkan_INCLUDE_DIR}) + +# detect version e.g 1.2.189 +set(Vulkan_VERSION "") +if(Vulkan_INCLUDE_DIR) + set(VULKAN_CORE_H ${Vulkan_INCLUDE_DIR}/vulkan/vulkan_core.h) + if(EXISTS ${VULKAN_CORE_H}) + file(STRINGS ${VULKAN_CORE_H} VulkanHeaderVersionLine REGEX "^#define VK_HEADER_VERSION ") + string(REGEX MATCHALL "[0-9]+" VulkanHeaderVersion "${VulkanHeaderVersionLine}") + file(STRINGS ${VULKAN_CORE_H} VulkanHeaderVersionLine2 REGEX "^#define VK_HEADER_VERSION_COMPLETE ") + string(REGEX MATCHALL "[0-9]+" VulkanHeaderVersion2 "${VulkanHeaderVersionLine2}") + list(LENGTH VulkanHeaderVersion2 _len) + # versions >= 1.2.175 have an additional numbers in front of e.g. '0, 1, 2' instead of '1, 2' + if(_len EQUAL 3) + list(REMOVE_AT VulkanHeaderVersion2 0) + endif() + list(APPEND VulkanHeaderVersion2 ${VulkanHeaderVersion}) + list(JOIN VulkanHeaderVersion2 "." Vulkan_VERSION) + endif() +endif() + +if(Vulkan_MoltenVK_FOUND) + set(Vulkan_MoltenVK_VERSION "") + if(Vulkan_MoltenVK_INCLUDE_DIR) + set(VK_MVK_MOLTENVK_H ${Vulkan_MoltenVK_INCLUDE_DIR}/MoltenVK/vk_mvk_moltenvk.h) + if(EXISTS ${VK_MVK_MOLTENVK_H}) + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_MAJOR REGEX "^#define MVK_VERSION_MAJOR ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_MAJOR "${_Vulkan_MoltenVK_VERSION_MAJOR}") + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_MINOR REGEX "^#define MVK_VERSION_MINOR ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_MINOR "${_Vulkan_MoltenVK_VERSION_MINOR}") + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_PATCH REGEX "^#define MVK_VERSION_PATCH ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_PATCH "${_Vulkan_MoltenVK_VERSION_PATCH}") + set(Vulkan_MoltenVK_VERSION "${_Vulkan_MoltenVK_VERSION_MAJOR}.${_Vulkan_MoltenVK_VERSION_MINOR}.${_Vulkan_MoltenVK_VERSION_PATCH}") + unset(_Vulkan_MoltenVK_VERSION_MAJOR) + unset(_Vulkan_MoltenVK_VERSION_MINOR) + unset(_Vulkan_MoltenVK_VERSION_PATCH) + endif() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Vulkan + REQUIRED_VARS + Vulkan_LIBRARY + Vulkan_INCLUDE_DIR + VERSION_VAR + Vulkan_VERSION + HANDLE_COMPONENTS +) + +if(Vulkan_FOUND AND NOT TARGET Vulkan::Vulkan) + add_library(Vulkan::Vulkan UNKNOWN IMPORTED) + set_target_properties(Vulkan::Vulkan PROPERTIES + IMPORTED_LOCATION "${Vulkan_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") +endif() + +if(Vulkan_FOUND AND NOT TARGET Vulkan::Headers) + add_library(Vulkan::Headers INTERFACE IMPORTED) + set_target_properties(Vulkan::Headers PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") +endif() + +if(Vulkan_FOUND AND Vulkan_GLSLC_EXECUTABLE AND NOT TARGET Vulkan::glslc) + add_executable(Vulkan::glslc IMPORTED) + set_property(TARGET Vulkan::glslc PROPERTY IMPORTED_LOCATION "${Vulkan_GLSLC_EXECUTABLE}") +endif() + +if(Vulkan_FOUND AND Vulkan_GLSLANG_VALIDATOR_EXECUTABLE AND NOT TARGET Vulkan::glslangValidator) + add_executable(Vulkan::glslangValidator IMPORTED) + set_property(TARGET Vulkan::glslangValidator PROPERTY IMPORTED_LOCATION "${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}") +endif() + +if(Vulkan_FOUND) + if((Vulkan_glslang-spirv_LIBRARY OR Vulkan_glslang-spirv_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-spirv) + add_library(Vulkan::glslang-spirv STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-spirv_LIBRARY) + set_property(TARGET Vulkan::glslang-spirv APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-spirv_LIBRARY}") + endif() + if(Vulkan_glslang-spirv_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-spirv APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-spirv_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-oglcompiler_LIBRARY OR Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-oglcompiler) + add_library(Vulkan::glslang-oglcompiler STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-oglcompiler_LIBRARY) + set_property(TARGET Vulkan::glslang-oglcompiler APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-oglcompiler_LIBRARY}") + endif() + if(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-oglcompiler APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-oglcompiler_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-osdependent_LIBRARY OR Vulkan_glslang-osdependent_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-osdependent) + add_library(Vulkan::glslang-osdependent STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-osdependent_LIBRARY) + set_property(TARGET Vulkan::glslang-osdependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-osdependent_LIBRARY}") + endif() + if(Vulkan_glslang-osdependent_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-osdependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-osdependent_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-machineindependent_LIBRARY OR Vulkan_glslang-machineindependent_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-machineindependent) + add_library(Vulkan::glslang-machineindependent STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-machineindependent_LIBRARY) + set_property(TARGET Vulkan::glslang-machineindependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-machineindependent_LIBRARY}") + endif() + if(Vulkan_glslang-machineindependent_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-machineindependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-machineindependent_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-genericcodegen_LIBRARY OR Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-genericcodegen) + add_library(Vulkan::glslang-genericcodegen STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-genericcodegen_LIBRARY) + set_property(TARGET Vulkan::glslang-genericcodegen APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-genericcodegen_LIBRARY}") + endif() + if(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-genericcodegen APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-genericcodegen_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang_LIBRARY OR Vulkan_glslang_DEBUG_LIBRARY) + AND TARGET Vulkan::glslang-spirv + AND TARGET Vulkan::glslang-oglcompiler + AND TARGET Vulkan::glslang-osdependent + AND TARGET Vulkan::glslang-machineindependent + AND TARGET Vulkan::glslang-genericcodegen + AND NOT TARGET Vulkan::glslang) + add_library(Vulkan::glslang STATIC IMPORTED) + set_property(TARGET Vulkan::glslang + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang_LIBRARY) + set_property(TARGET Vulkan::glslang APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang_LIBRARY}") + endif() + if(Vulkan_glslang_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang_DEBUG_LIBRARY}") + endif() + target_link_libraries(Vulkan::glslang + INTERFACE + Vulkan::glslang-spirv + Vulkan::glslang-oglcompiler + Vulkan::glslang-osdependent + Vulkan::glslang-machineindependent + Vulkan::glslang-genericcodegen + ) + endif() + + if((Vulkan_shaderc_combined_LIBRARY OR Vulkan_shaderc_combined_DEBUG_LIBRARY) AND NOT TARGET Vulkan::shaderc_combined) + add_library(Vulkan::shaderc_combined STATIC IMPORTED) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_shaderc_combined_LIBRARY) + set_property(TARGET Vulkan::shaderc_combined APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_shaderc_combined_LIBRARY}") + endif() + if(Vulkan_shaderc_combined_DEBUG_LIBRARY) + set_property(TARGET Vulkan::shaderc_combined APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_shaderc_combined_DEBUG_LIBRARY}") + endif() + + if(UNIX) + find_package(Threads REQUIRED) + target_link_libraries(Vulkan::shaderc_combined + INTERFACE + Threads::Threads) + endif() + endif() + + if((Vulkan_SPIRV-Tools_LIBRARY OR Vulkan_SPIRV-Tools_DEBUG_LIBRARY) AND NOT TARGET Vulkan::SPIRV-Tools) + add_library(Vulkan::SPIRV-Tools STATIC IMPORTED) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_SPIRV-Tools_LIBRARY) + set_property(TARGET Vulkan::SPIRV-Tools APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_SPIRV-Tools_LIBRARY}") + endif() + if(Vulkan_SPIRV-Tools_DEBUG_LIBRARY) + set_property(TARGET Vulkan::SPIRV-Tools APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_SPIRV-Tools_DEBUG_LIBRARY}") + endif() + endif() + + if(Vulkan_volk_LIBRARY AND NOT TARGET Vulkan::volk) + add_library(Vulkan::volk STATIC IMPORTED) + set_property(TARGET Vulkan::volk + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_volk_LIBRARY}") + + if (NOT WIN32) + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_LINK_INTERFACE_LIBRARIES dl) + endif() + endif() + + if (Vulkan_dxc_LIBRARY AND NOT TARGET Vulkan::dxc_lib) + add_library(Vulkan::dxc_lib STATIC IMPORTED) + set_property(TARGET Vulkan::dxc_lib + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + set_property(TARGET Vulkan::dxc_lib APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::dxc_lib APPEND + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_dxc_LIBRARY}") + endif() + + if(Vulkan_dxc_EXECUTABLE AND NOT TARGET Vulkan::dxc_exe) + add_executable(Vulkan::dxc_exe IMPORTED) + set_property(TARGET Vulkan::dxc_exe PROPERTY IMPORTED_LOCATION "${Vulkan_dxc_EXECUTABLE}") + endif() + +endif() + +if(Vulkan_MoltenVK_FOUND) + if(Vulkan_MoltenVK_LIBRARY AND NOT TARGET Vulkan::MoltenVK) + add_library(Vulkan::MoltenVK SHARED IMPORTED) + set_target_properties(Vulkan::MoltenVK + PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_MoltenVK_INCLUDE_DIR}" + IMPORTED_LOCATION "${Vulkan_MoltenVK_LIBRARY}" + ) + endif() +endif() + +unset(_Vulkan_library_name) +unset(_Vulkan_hint_include_search_paths) +unset(_Vulkan_hint_executable_search_paths) +unset(_Vulkan_hint_library_search_paths) + +cmake_policy(POP) \ No newline at end of file diff --git a/attachments/simple_engine/CMake/FindVulkanHpp.cmake b/attachments/simple_engine/CMake/FindVulkanHpp.cmake new file mode 100644 index 00000000..2c0e23ab --- /dev/null +++ b/attachments/simple_engine/CMake/FindVulkanHpp.cmake @@ -0,0 +1,426 @@ +# FindVulkanHpp.cmake +# +# Finds or downloads the Vulkan-Hpp headers and Vulkan Profiles headers +# +# This will define the following variables +# +# VulkanHpp_FOUND +# VulkanHpp_INCLUDE_DIRS +# +# and the following imported targets +# +# VulkanHpp::VulkanHpp +# + +# Try to find the package using standard find_path +find_path(VulkanHpp_INCLUDE_DIR + NAMES vulkan/vulkan.hpp + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Also try to find vulkan.cppm +find_path(VulkanHpp_CPPM_DIR + NAMES vulkan/vulkan.cppm + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Try to find vulkan_profiles.hpp +find_path(VulkanProfiles_INCLUDE_DIR + NAMES vulkan/vulkan_profiles.hpp + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Function to extract Vulkan version from vulkan_core.h +function(extract_vulkan_version VULKAN_CORE_H_PATH OUTPUT_VERSION_TAG) + # Extract the version information from vulkan_core.h + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_VERSION_MAJOR_LINE REGEX "^#define VK_VERSION_MAJOR") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_VERSION_MINOR_LINE REGEX "^#define VK_VERSION_MINOR") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_LINE REGEX "^#define VK_HEADER_VERSION") + + set(VERSION_TAG "v1.3.275") # Default fallback + + if(VULKAN_VERSION_MAJOR_LINE AND VULKAN_VERSION_MINOR_LINE AND VULKAN_HEADER_VERSION_LINE) + string(REGEX REPLACE "^#define VK_VERSION_MAJOR[ \t]+([0-9]+).*$" "\\1" VULKAN_VERSION_MAJOR "${VULKAN_VERSION_MAJOR_LINE}") + string(REGEX REPLACE "^#define VK_VERSION_MINOR[ \t]+([0-9]+).*$" "\\1" VULKAN_VERSION_MINOR "${VULKAN_VERSION_MINOR_LINE}") + string(REGEX REPLACE "^#define VK_HEADER_VERSION[ \t]+([0-9]+).*$" "\\1" VULKAN_HEADER_VERSION "${VULKAN_HEADER_VERSION_LINE}") + + # Construct the version tag + set(VERSION_TAG "v${VULKAN_VERSION_MAJOR}.${VULKAN_VERSION_MINOR}.${VULKAN_HEADER_VERSION}") + else() + # Alternative approach: look for VK_HEADER_VERSION_COMPLETE + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_COMPLETE_LINE REGEX "^#define VK_HEADER_VERSION_COMPLETE") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_LINE REGEX "^#define VK_HEADER_VERSION") + + if(VULKAN_HEADER_VERSION_COMPLETE_LINE AND VULKAN_HEADER_VERSION_LINE) + # Extract the header version + string(REGEX REPLACE "^#define VK_HEADER_VERSION[ \t]+([0-9]+).*$" "\\1" VULKAN_HEADER_VERSION "${VULKAN_HEADER_VERSION_LINE}") + + # Check if the complete version line contains the major and minor versions + if(VULKAN_HEADER_VERSION_COMPLETE_LINE MATCHES "VK_MAKE_API_VERSION\\(.*,[ \t]*([0-9]+),[ \t]*([0-9]+),[ \t]*VK_HEADER_VERSION\\)") + set(VULKAN_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(VULKAN_VERSION_MINOR "${CMAKE_MATCH_2}") + set(VERSION_TAG "v${VULKAN_VERSION_MAJOR}.${VULKAN_VERSION_MINOR}.${VULKAN_HEADER_VERSION}") + endif() + endif() + endif() + + # Return the version tag + set(${OUTPUT_VERSION_TAG} ${VERSION_TAG} PARENT_SCOPE) +endfunction() + +# Determine the Vulkan version to use for Vulkan-Hpp and Vulkan-Profiles +set(VULKAN_VERSION_TAG "v1.3.275") # Default version + +# Try to detect the Vulkan version +set(VULKAN_CORE_H "") + +# If we're building for Android, try to detect the NDK's Vulkan version +if(DEFINED ANDROID_NDK) + # Find the vulkan_core.h file in the NDK + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + ${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/windows/sysroot/usr/include/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected NDK Vulkan version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in NDK, using default version: ${VULKAN_VERSION_TAG}") + endif() +# For desktop builds, try to detect the Vulkan SDK version +elseif(DEFINED ENV{VULKAN_SDK}) + # Find the vulkan_core.h file in the Vulkan SDK + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + $ENV{VULKAN_SDK}/include/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected Vulkan SDK version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in Vulkan SDK, using default version: ${VULKAN_VERSION_TAG}") + endif() +# If Vulkan package was already found, try to use its include directory +elseif(DEFINED Vulkan_INCLUDE_DIR) + # Find the vulkan_core.h file in the Vulkan include directory + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + ${Vulkan_INCLUDE_DIR}/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected Vulkan version from include directory: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in Vulkan include directory, using default version: ${VULKAN_VERSION_TAG}") + endif() +else() + # Try to find vulkan_core.h in system paths + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + /usr/include/vulkan + /usr/local/include/vulkan + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected system Vulkan version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in system paths, using default version: ${VULKAN_VERSION_TAG}") + endif() +endif() + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT VulkanHpp_INCLUDE_DIR OR NOT VulkanHpp_CPPM_DIR) + # If not found, use FetchContent to download + include(FetchContent) + + message(STATUS "Vulkan-Hpp not found, fetching from GitHub with version ${VULKAN_VERSION_TAG}...") + FetchContent_Declare( + VulkanHpp + GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Hpp.git + GIT_TAG ${VULKAN_VERSION_TAG} # Use the detected or default version + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Make sure FetchContent is available + include(FetchContent) + + # Populate the content + FetchContent_GetProperties(VulkanHpp SOURCE_DIR VulkanHpp_SOURCE_DIR) + if(NOT VulkanHpp_POPULATED) + FetchContent_Populate(VulkanHpp) + # Get the source directory after populating + FetchContent_GetProperties(VulkanHpp SOURCE_DIR VulkanHpp_SOURCE_DIR) + endif() + + # Set the include directory to the source directory + set(VulkanHpp_INCLUDE_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "VulkanHpp_SOURCE_DIR: ${VulkanHpp_SOURCE_DIR}") + message(STATUS "VulkanHpp_INCLUDE_DIR: ${VulkanHpp_INCLUDE_DIR}") + + # Check if vulkan.cppm exists in the downloaded repository + if(EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() +endif() + +# If the Vulkan Profiles include directory wasn't found, use FetchContent to download +if(NOT VulkanProfiles_INCLUDE_DIR) + # If not found, use FetchContent to download + include(FetchContent) + + message(STATUS "Vulkan-Profiles not found, fetching from GitHub main branch...") + FetchContent_Declare( + VulkanProfiles + GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Profiles.git + GIT_TAG main # Use main branch instead of a specific tag + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content + FetchContent_GetProperties(VulkanProfiles SOURCE_DIR VulkanProfiles_SOURCE_DIR) + if(NOT VulkanProfiles_POPULATED) + FetchContent_Populate(VulkanProfiles) + # Get the source directory after populating + FetchContent_GetProperties(VulkanProfiles SOURCE_DIR VulkanProfiles_SOURCE_DIR) + endif() + + # Create the include directory structure if it doesn't exist + set(VulkanProfiles_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanProfiles/include) + file(MAKE_DIRECTORY ${VulkanProfiles_INCLUDE_DIR}/vulkan) + + # Create a stub vulkan_profiles.hpp file if it doesn't exist + if(NOT EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + file(WRITE "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp" +"// Auto-generated vulkan_profiles.hpp stub file +#pragma once +#include + +namespace vp { + // Stub implementation for Vulkan Profiles + struct ProfileDesc { + const char* name; + uint32_t specVersion; + }; + + inline bool GetProfileSupport(VkPhysicalDevice physicalDevice, const ProfileDesc* pProfile, VkBool32* pSupported) { + *pSupported = VK_TRUE; + return true; + } +} +") + endif() + + message(STATUS "VulkanProfiles_SOURCE_DIR: ${VulkanProfiles_SOURCE_DIR}") + message(STATUS "VulkanProfiles_INCLUDE_DIR: ${VulkanProfiles_INCLUDE_DIR}") +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(VulkanHpp + REQUIRED_VARS VulkanHpp_INCLUDE_DIR + FAIL_MESSAGE "Could NOT find VulkanHpp. Install it or set VulkanHpp_INCLUDE_DIR to the directory containing vulkan/vulkan.hpp" +) + +# Debug output +message(STATUS "VulkanHpp_FOUND: ${VulkanHpp_FOUND}") +message(STATUS "VULKANHPP_FOUND: ${VULKANHPP_FOUND}") + +if(VulkanHpp_FOUND) + set(VulkanHpp_INCLUDE_DIRS ${VulkanHpp_INCLUDE_DIR}) + + # Make sure VulkanHpp_CPPM_DIR is set + if(NOT DEFINED VulkanHpp_CPPM_DIR) + # Check if vulkan.cppm exists in the include directory + if(EXISTS "${VulkanHpp_INCLUDE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_INCLUDE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_INCLUDE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED VulkanHpp_SOURCE_DIR AND EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED vulkanhpp_SOURCE_DIR AND EXISTS "${vulkanhpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${vulkanhpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in vulkanhpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + message(STATUS "Creating vulkan.cppm in ${VulkanHpp_CPPM_DIR}") + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() + endif() + + message(STATUS "Final VulkanHpp_CPPM_DIR: ${VulkanHpp_CPPM_DIR}") + + # Add Vulkan Profiles include directory if found + if(VulkanProfiles_INCLUDE_DIR AND EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + list(APPEND VulkanHpp_INCLUDE_DIRS ${VulkanProfiles_INCLUDE_DIR}) + message(STATUS "Added Vulkan Profiles include directory: ${VulkanProfiles_INCLUDE_DIR}") + endif() + + # Create an imported target + if(NOT TARGET VulkanHpp::VulkanHpp) + add_library(VulkanHpp::VulkanHpp INTERFACE IMPORTED) + set_target_properties(VulkanHpp::VulkanHpp PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${VulkanHpp_INCLUDE_DIRS}" + ) + endif() +elseif(DEFINED VulkanHpp_SOURCE_DIR OR DEFINED vulkanhpp_SOURCE_DIR) + # If find_package_handle_standard_args failed but we have a VulkanHpp source directory from FetchContent + # Create an imported target + if(NOT TARGET VulkanHpp::VulkanHpp) + add_library(VulkanHpp::VulkanHpp INTERFACE IMPORTED) + + # Determine the source directory + if(DEFINED VulkanHpp_SOURCE_DIR) + set(_vulkanhpp_source_dir ${VulkanHpp_SOURCE_DIR}) + elseif(DEFINED vulkanhpp_SOURCE_DIR) + set(_vulkanhpp_source_dir ${vulkanhpp_SOURCE_DIR}) + endif() + + message(STATUS "Using fallback VulkanHpp source directory: ${_vulkanhpp_source_dir}") + + set_target_properties(VulkanHpp::VulkanHpp PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_vulkanhpp_source_dir}" + ) + endif() + + # Set variables to indicate that VulkanHpp was found + set(VulkanHpp_FOUND TRUE) + set(VULKANHPP_FOUND TRUE) + + # Set include directories + if(DEFINED _vulkanhpp_source_dir) + set(VulkanHpp_INCLUDE_DIR ${_vulkanhpp_source_dir}) + elseif(DEFINED VulkanHpp_SOURCE_DIR) + set(VulkanHpp_INCLUDE_DIR ${VulkanHpp_SOURCE_DIR}) + elseif(DEFINED vulkanhpp_SOURCE_DIR) + set(VulkanHpp_INCLUDE_DIR ${vulkanhpp_SOURCE_DIR}) + endif() + set(VulkanHpp_INCLUDE_DIRS ${VulkanHpp_INCLUDE_DIR}) + + # Add Vulkan Profiles include directory if found + if(VulkanProfiles_INCLUDE_DIR AND EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + list(APPEND VulkanHpp_INCLUDE_DIRS ${VulkanProfiles_INCLUDE_DIR}) + message(STATUS "Added Vulkan Profiles include directory to fallback: ${VulkanProfiles_INCLUDE_DIR}") + endif() + + # Make sure VulkanHpp_CPPM_DIR is set + if(NOT DEFINED VulkanHpp_CPPM_DIR) + # Check if vulkan.cppm exists in the downloaded repository + if(DEFINED VulkanHpp_INCLUDE_DIR AND EXISTS "${VulkanHpp_INCLUDE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_INCLUDE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_INCLUDE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED _vulkanhpp_source_dir AND EXISTS "${_vulkanhpp_source_dir}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${_vulkanhpp_source_dir}) + message(STATUS "Found vulkan.cppm in _vulkanhpp_source_dir: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED VulkanHpp_SOURCE_DIR AND EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED vulkanhpp_SOURCE_DIR AND EXISTS "${vulkanhpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${vulkanhpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in vulkanhpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + message(STATUS "Creating vulkan.cppm in ${VulkanHpp_CPPM_DIR}") + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() + endif() + + message(STATUS "Final VulkanHpp_CPPM_DIR: ${VulkanHpp_CPPM_DIR}") +endif() + +mark_as_advanced(VulkanHpp_INCLUDE_DIR VulkanHpp_CPPM_DIR) diff --git a/attachments/simple_engine/CMake/Findglm.cmake b/attachments/simple_engine/CMake/Findglm.cmake new file mode 100644 index 00000000..fdf113cf --- /dev/null +++ b/attachments/simple_engine/CMake/Findglm.cmake @@ -0,0 +1,133 @@ +# Findglm.cmake +# +# Finds the GLM library +# +# This will define the following variables +# +# glm_FOUND +# glm_INCLUDE_DIRS +# +# and the following imported targets +# +# glm::glm +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_glm QUIET glm) +endif() + +# Find the include directory +find_path(glm_INCLUDE_DIR + NAMES glm/glm.hpp + PATHS + ${PC_glm_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES glm +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT glm_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "GLM not found, fetching from GitHub...") + FetchContent_Declare( + glm + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG 0.9.9.8 # Use a specific tag for stability + ) + + # Define a function to update the CMake minimum required version + function(update_glm_cmake_version) + # Get the source directory + FetchContent_GetProperties(glm SOURCE_DIR glm_SOURCE_DIR) + + # Update the minimum required CMake version + file(READ "${glm_SOURCE_DIR}/CMakeLists.txt" GLM_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.2" + "cmake_minimum_required(VERSION 3.5" + GLM_CMAKE_CONTENT "${GLM_CMAKE_CONTENT}") + file(WRITE "${glm_SOURCE_DIR}/CMakeLists.txt" "${GLM_CMAKE_CONTENT}") + endfunction() + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # First, declare and populate the content + FetchContent_GetProperties(glm) + if(NOT glm_POPULATED) + FetchContent_Populate(glm) + # Update the CMake version before making it available + update_glm_cmake_version() + endif() + + # Now make it available (this will process the CMakeLists.txt) + FetchContent_MakeAvailable(glm) + + # Get the include directory from the target + if(TARGET glm) + get_target_property(glm_INCLUDE_DIR glm INTERFACE_INCLUDE_DIRECTORIES) + if(NOT glm_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + endif() + else() + # GLM might not create a target, so use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(glm + REQUIRED_VARS glm_INCLUDE_DIR +) + +if(glm_FOUND) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET glm::glm) + add_library(glm::glm INTERFACE IMPORTED) + set_target_properties(glm::glm PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${glm_INCLUDE_DIRS}" + ) + endif() +elseif(TARGET glm) + # If find_package_handle_standard_args failed but we have a glm target from FetchContent + # Create an alias for the glm target + if(NOT TARGET glm::glm) + add_library(glm::glm ALIAS glm) + endif() + + # Set variables to indicate that glm was found + set(glm_FOUND TRUE) + set(GLM_FOUND TRUE) + + # Set include directories + get_target_property(glm_INCLUDE_DIR glm INTERFACE_INCLUDE_DIRECTORIES) + if(glm_INCLUDE_DIR) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(glm_INCLUDE_DIR) diff --git a/attachments/simple_engine/CMake/Findnlohmann_json.cmake b/attachments/simple_engine/CMake/Findnlohmann_json.cmake new file mode 100644 index 00000000..61dc66a6 --- /dev/null +++ b/attachments/simple_engine/CMake/Findnlohmann_json.cmake @@ -0,0 +1,154 @@ +# Findnlohmann_json.cmake +# +# Finds the nlohmann_json library +# +# This will define the following variables +# +# nlohmann_json_FOUND +# nlohmann_json_INCLUDE_DIRS +# +# and the following imported targets +# +# nlohmann_json::nlohmann_json +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_nlohmann_json QUIET nlohmann_json) +endif() + +# Find the include directory +find_path(nlohmann_json_INCLUDE_DIR + NAMES nlohmann/json.hpp json.hpp + PATHS + ${PC_nlohmann_json_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES nlohmann json +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT nlohmann_json_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "nlohmann_json not found, fetching from GitHub...") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 # Use a specific tag for stability + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(nlohmann_json) + if(NOT nlohmann_json_POPULATED) + FetchContent_Populate(nlohmann_json) + + if(ANDROID) + # Update the minimum required CMake version before including the CMakeLists.txt + file(READ "${nlohmann_json_SOURCE_DIR}/CMakeLists.txt" NLOHMANN_JSON_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.1" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.2" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.3" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.4" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.5" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.6" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.7" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.8" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.9" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + file(WRITE "${nlohmann_json_SOURCE_DIR}/CMakeLists.txt" "${NLOHMANN_JSON_CMAKE_CONTENT}") + endif() + + # Now add the subdirectory manually + add_subdirectory(${nlohmann_json_SOURCE_DIR} ${nlohmann_json_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(nlohmann_json) + endif() + + # Get the include directory from the target + if(TARGET nlohmann_json) + get_target_property(nlohmann_json_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) + if(NOT nlohmann_json_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + endif() + else() + # nlohmann_json might not create a target, so use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(nlohmann_json + REQUIRED_VARS nlohmann_json_INCLUDE_DIR +) + +if(nlohmann_json_FOUND) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET nlohmann_json::nlohmann_json) + add_library(nlohmann_json::nlohmann_json INTERFACE IMPORTED) + set_target_properties(nlohmann_json::nlohmann_json PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${nlohmann_json_INCLUDE_DIRS}" + ) + endif() +elseif(TARGET nlohmann_json) + # If find_package_handle_standard_args failed but we have a nlohmann_json target from FetchContent + # Create an alias for the nlohmann_json target + if(NOT TARGET nlohmann_json::nlohmann_json) + add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) + endif() + + # Set variables to indicate that nlohmann_json was found + set(nlohmann_json_FOUND TRUE) + set(NLOHMANN_JSON_FOUND TRUE) + + # Set include directories + get_target_property(nlohmann_json_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) + if(nlohmann_json_INCLUDE_DIR) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(nlohmann_json_INCLUDE_DIR) diff --git a/attachments/simple_engine/CMake/Findstb.cmake b/attachments/simple_engine/CMake/Findstb.cmake new file mode 100644 index 00000000..6ccf72f5 --- /dev/null +++ b/attachments/simple_engine/CMake/Findstb.cmake @@ -0,0 +1,86 @@ +# Findstb.cmake +# +# Finds the stb library (specifically stb_image.h) +# +# This will define the following variables +# +# stb_FOUND +# stb_INCLUDE_DIRS +# +# and the following imported targets +# +# stb::stb +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_stb QUIET stb) +endif() + +# Find the include directory +find_path(stb_INCLUDE_DIR + NAMES stb_image.h + PATHS + ${PC_stb_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES stb +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT stb_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "stb_image.h not found, fetching from GitHub...") + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master # stb doesn't use version tags, so we use master + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content + FetchContent_GetProperties(stb) + if(NOT stb_POPULATED) + FetchContent_Populate(stb) + endif() + + # stb is a header-only library with no CMakeLists.txt, so we just need to set the include directory + set(stb_INCLUDE_DIR ${stb_SOURCE_DIR}) +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(stb + REQUIRED_VARS stb_INCLUDE_DIR +) + +if(stb_FOUND) + set(stb_INCLUDE_DIRS ${stb_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET stb::stb) + add_library(stb::stb INTERFACE IMPORTED) + set_target_properties(stb::stb PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${stb_INCLUDE_DIRS}" + ) + endif() +endif() + +mark_as_advanced(stb_INCLUDE_DIR) \ No newline at end of file diff --git a/attachments/simple_engine/CMake/Findtinygltf.cmake b/attachments/simple_engine/CMake/Findtinygltf.cmake new file mode 100644 index 00000000..b2412350 --- /dev/null +++ b/attachments/simple_engine/CMake/Findtinygltf.cmake @@ -0,0 +1,162 @@ +# Findtinygltf.cmake +# +# Finds the tinygltf library +# +# This will define the following variables +# +# tinygltf_FOUND +# tinygltf_INCLUDE_DIRS +# +# and the following imported targets +# +# tinygltf::tinygltf +# + +# First, try to find nlohmann_json +find_package(nlohmann_json QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + message(STATUS "nlohmann_json not found, fetching v3.12.0 from GitHub...") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 # Use a specific tag for stability + ) + FetchContent_MakeAvailable(nlohmann_json) +endif() + +# Try to find tinygltf using standard find_package +find_path(tinygltf_INCLUDE_DIR + NAMES tiny_gltf.h + PATHS + ${PC_tinygltf_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES tinygltf include +) + +# If not found, use FetchContent to download and build +if(NOT tinygltf_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "tinygltf not found, fetching from GitHub...") + FetchContent_Declare( + tinygltf + GIT_REPOSITORY https://github.com/syoyo/tinygltf.git + GIT_TAG v2.8.18 # Use a specific tag for stability + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(tinygltf) + if(NOT tinygltf_POPULATED) + FetchContent_Populate(tinygltf) + + # Update the minimum required CMake version to avoid deprecation warning + file(READ "${tinygltf_SOURCE_DIR}/CMakeLists.txt" TINYGLTF_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.6)" + "cmake_minimum_required(VERSION 3.10)" + TINYGLTF_CMAKE_CONTENT "${TINYGLTF_CMAKE_CONTENT}") + file(WRITE "${tinygltf_SOURCE_DIR}/CMakeLists.txt" "${TINYGLTF_CMAKE_CONTENT}") + + # Create a symbolic link to make nlohmann/json.hpp available + if(EXISTS "${tinygltf_SOURCE_DIR}/json.hpp") + file(MAKE_DIRECTORY "${tinygltf_SOURCE_DIR}/nlohmann") + file(CREATE_LINK "${tinygltf_SOURCE_DIR}/json.hpp" "${tinygltf_SOURCE_DIR}/nlohmann/json.hpp" SYMBOLIC) + endif() + + # Set tinygltf to header-only mode + set(TINYGLTF_HEADER_ONLY ON CACHE BOOL "Use header only version" FORCE) + set(TINYGLTF_INSTALL OFF CACHE BOOL "Do not install tinygltf" FORCE) + + # Add the subdirectory after modifying the CMakeLists.txt + add_subdirectory(${tinygltf_SOURCE_DIR} ${tinygltf_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(tinygltf) + endif() + + # Get the include directory from the target + get_target_property(tinygltf_INCLUDE_DIR tinygltf INTERFACE_INCLUDE_DIRECTORIES) + if(NOT tinygltf_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + FetchContent_GetProperties(tinygltf SOURCE_DIR tinygltf_SOURCE_DIR) + set(tinygltf_INCLUDE_DIR ${tinygltf_SOURCE_DIR}) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(tinygltf + REQUIRED_VARS tinygltf_INCLUDE_DIR +) + +if(tinygltf_FOUND) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf::tinygltf INTERFACE IMPORTED) + set_target_properties(tinygltf::tinygltf PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinygltf_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "TINYGLTF_IMPLEMENTATION;TINYGLTF_NO_EXTERNAL_IMAGE;TINYGLTF_NO_STB_IMAGE;TINYGLTF_NO_STB_IMAGE_WRITE" + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf::tinygltf INTERFACE nlohmann_json::nlohmann_json) + endif() + endif() +elseif(TARGET tinygltf) + # If find_package_handle_standard_args failed but we have a tinygltf target from FetchContent + # Create an alias for the tinygltf target + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf_wrapper INTERFACE) + target_link_libraries(tinygltf_wrapper INTERFACE tinygltf) + target_compile_definitions(tinygltf_wrapper INTERFACE + TINYGLTF_IMPLEMENTATION + TINYGLTF_NO_EXTERNAL_IMAGE + TINYGLTF_NO_STB_IMAGE + TINYGLTF_NO_STB_IMAGE_WRITE + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf_wrapper INTERFACE nlohmann_json::nlohmann_json) + endif() + add_library(tinygltf::tinygltf ALIAS tinygltf_wrapper) + endif() + + # Set variables to indicate that tinygltf was found + set(tinygltf_FOUND TRUE) + set(TINYGLTF_FOUND TRUE) + + # Set include directories + get_target_property(tinygltf_INCLUDE_DIR tinygltf INTERFACE_INCLUDE_DIRECTORIES) + if(tinygltf_INCLUDE_DIR) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + FetchContent_GetProperties(tinygltf SOURCE_DIR tinygltf_SOURCE_DIR) + set(tinygltf_INCLUDE_DIR ${tinygltf_SOURCE_DIR}) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + + # Explicitly set the include directory on the target + if(TARGET tinygltf) + set_target_properties(tinygltf PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinygltf_INCLUDE_DIR}" + ) + endif() +endif() +endif() + +mark_as_advanced(tinygltf_INCLUDE_DIR) diff --git a/attachments/simple_engine/CMake/Findtinyobjloader.cmake b/attachments/simple_engine/CMake/Findtinyobjloader.cmake new file mode 100644 index 00000000..4b1fb44c --- /dev/null +++ b/attachments/simple_engine/CMake/Findtinyobjloader.cmake @@ -0,0 +1,160 @@ +# Findtinyobjloader.cmake +# Find the tinyobjloader library +# +# This module defines the following variables: +# tinyobjloader_FOUND - True if tinyobjloader was found +# tinyobjloader_INCLUDE_DIRS - Include directories for tinyobjloader +# tinyobjloader_LIBRARIES - Libraries to link against tinyobjloader +# +# It also defines the following targets: +# tinyobjloader::tinyobjloader + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_tinyobjloader QUIET tinyobjloader) +endif() + +# Find the include directory +find_path(tinyobjloader_INCLUDE_DIR + NAMES tiny_obj_loader.h + PATHS + ${PC_tinyobjloader_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES tinyobjloader tiny_obj_loader +) + +# Find the library +find_library(tinyobjloader_LIBRARY + NAMES tinyobjloader + PATHS + ${PC_tinyobjloader_LIBRARY_DIRS} + /usr/lib + /usr/local/lib + $ENV{VULKAN_SDK}/lib + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/lib + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../lib + PATH_SUFFIXES lib +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT tinyobjloader_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "tinyobjloader not found, fetching from GitHub...") + FetchContent_Declare( + tinyobjloader + GIT_REPOSITORY https://github.com/tinyobjloader/tinyobjloader.git + GIT_TAG v2.0.0rc10 # Use a specific tag for stability + ) + + # Set options before making tinyobjloader available + set(TINYOBJLOADER_BUILD_TEST_LOADER OFF CACHE BOOL "Do not build test loader" FORCE) + set(TINYOBJLOADER_BUILD_OBJ_STICHER OFF CACHE BOOL "Do not build obj sticher" FORCE) + set(TINYOBJLOADER_INSTALL OFF CACHE BOOL "Do not install tinyobjloader" FORCE) + + # Update CMake policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(tinyobjloader) + if(NOT tinyobjloader_POPULATED) + FetchContent_Populate(tinyobjloader) + + # Update the minimum required CMake version before including the CMakeLists.txt + file(READ "${tinyobjloader_SOURCE_DIR}/CMakeLists.txt" TINYOBJLOADER_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.2)" + "cmake_minimum_required(VERSION 3.10)" + TINYOBJLOADER_CMAKE_CONTENT "${TINYOBJLOADER_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.5)" + "cmake_minimum_required(VERSION 3.10)" + TINYOBJLOADER_CMAKE_CONTENT "${TINYOBJLOADER_CMAKE_CONTENT}") + file(WRITE "${tinyobjloader_SOURCE_DIR}/CMakeLists.txt" "${TINYOBJLOADER_CMAKE_CONTENT}") + + # Now add the subdirectory manually + add_subdirectory(${tinyobjloader_SOURCE_DIR} ${tinyobjloader_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(tinyobjloader) + endif() + + # Get the include directory from the target + get_target_property(tinyobjloader_INCLUDE_DIR tinyobjloader INTERFACE_INCLUDE_DIRECTORIES) + if(NOT tinyobjloader_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(tinyobjloader_INCLUDE_DIR ${tinyobjloader_SOURCE_DIR}) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(tinyobjloader + REQUIRED_VARS tinyobjloader_INCLUDE_DIR +) + +if(tinyobjloader_FOUND) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + + if(tinyobjloader_LIBRARY) + set(tinyobjloader_LIBRARIES ${tinyobjloader_LIBRARY}) + else() + # tinyobjloader is a header-only library, so no library is needed + set(tinyobjloader_LIBRARIES "") + endif() + + # Create an imported target + if(NOT TARGET tinyobjloader::tinyobjloader) + add_library(tinyobjloader::tinyobjloader INTERFACE IMPORTED) + set_target_properties(tinyobjloader::tinyobjloader PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinyobjloader_INCLUDE_DIRS}" + ) + if(tinyobjloader_LIBRARIES) + set_target_properties(tinyobjloader::tinyobjloader PROPERTIES + INTERFACE_LINK_LIBRARIES "${tinyobjloader_LIBRARIES}" + ) + endif() + endif() +elseif(TARGET tinyobjloader) + # If find_package_handle_standard_args failed but we have a tinyobjloader target from FetchContent + # Create an alias for the tinyobjloader target + if(NOT TARGET tinyobjloader::tinyobjloader) + add_library(tinyobjloader::tinyobjloader ALIAS tinyobjloader) + endif() + + # Set variables to indicate that tinyobjloader was found + set(tinyobjloader_FOUND TRUE) + set(TINYOBJLOADER_FOUND TRUE) + + # Set include directories + get_target_property(tinyobjloader_INCLUDE_DIR tinyobjloader INTERFACE_INCLUDE_DIRECTORIES) + if(tinyobjloader_INCLUDE_DIR) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(tinyobjloader_INCLUDE_DIR ${tinyobjloader_SOURCE_DIR}) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(tinyobjloader_INCLUDE_DIR tinyobjloader_LIBRARY) From 606be30862077ef1af9b74de422cac80cd38e2c1 Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 16 Dec 2025 14:50:38 -0800 Subject: [PATCH 04/24] Add advanced topics documentation: Ray Query, Planar Reflections, Robustness2, Mipmaps and LOD, glTF Animation, and Push Constants. --- .../Advanced_Topics/01_introduction.adoc | 27 ++ .../Advanced_Topics/Culling.adoc | 46 ++++ .../Descriptor_Indexing_UpdateAfterBind.adoc | 73 +++++ .../Dynamic_Rendering_Local_Read.adoc | 39 +++ .../ForwardPlus_Rendering.adoc | 37 +++ .../Forward_ForwardPlus_Deferred.adoc | 102 +++++++ .../Advanced_Topics/GLTF_Animation.adoc | 259 ++++++++++++++++++ .../Advanced_Topics/Mipmaps_and_LOD.adoc | 39 +++ .../Advanced_Topics/Planar_Reflections.adoc | 160 +++++++++++ .../Push_Constants_Per_Object.adoc | 39 +++ ...ay_Query_Reflections_and_Transparency.adoc | 87 ++++++ .../Advanced_Topics/Ray_Query_Rendering.adoc | 93 +++++++ .../Rendering_Pipeline_Overview.adoc | 56 ++++ .../Advanced_Topics/Robustness2.adoc | 51 ++++ .../Separate_Image_Sampler_Descriptors.adoc | 42 +++ .../Advanced_Topics/Shader_Tile_Image.adoc | 40 +++ .../Synchronization_2_Frame_Pacing.adoc | 65 +++++ .../Synchronization_and_Streaming.adoc | 91 ++++++ .../Mobile_Development/06_conclusion.adoc | 8 + en/Building_a_Simple_Engine/introduction.adoc | 2 + 20 files changed, 1356 insertions(+) create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Culling.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Descriptor_Indexing_UpdateAfterBind.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/ForwardPlus_Rendering.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Forward_ForwardPlus_Deferred.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/GLTF_Animation.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Mipmaps_and_LOD.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Planar_Reflections.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Push_Constants_Per_Object.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Reflections_and_Transparency.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Rendering.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Rendering_Pipeline_Overview.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Separate_Image_Sampler_Descriptors.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Shader_Tile_Image.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_2_Frame_Pacing.adoc create mode 100644 en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_and_Streaming.adoc diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc new file mode 100644 index 00000000..30399ff0 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/01_introduction.adoc @@ -0,0 +1,27 @@ +::pp: {plus}{plus} + += Advanced Topics (Simple Engine) + +Welcome — this section collects short, conversational guides that explain what each feature is, why we use it, and how it’s implemented in the Simple Engine. + +Start anywhere that matches your interest: + +* xref:Planar_Reflections.adoc[Planar Reflections] +* xref:Ray_Query_Rendering.adoc[Ray Query Rendering] +* xref:Ray_Query_Reflections_and_Transparency.adoc[Ray Query Reflections and Transparency] +* xref:Rendering_Pipeline_Overview.adoc[Rendering Pipeline Overview] +* xref:Forward_ForwardPlus_Deferred.adoc[Forward, Forward+, Deferred] +* xref:ForwardPlus_Rendering.adoc[Forward+ Rendering] +* xref:Culling.adoc[Frustum Culling and Distance LOD] +* xref:Mipmaps_and_LOD.adoc[Mipmaps and LOD] +* xref:GLTF_Animation.adoc[glTF Animation & Transform Composition] +* xref:Push_Constants_Per_Object.adoc[Push Constants (per‑object material)] +* xref:Descriptor_Indexing_UpdateAfterBind.adoc[Descriptor Indexing & Stable Updates] +* xref:Separate_Image_Sampler_Descriptors.adoc[Separate Image/Sampler] +* xref:Synchronization_and_Streaming.adoc[Synchronization & Streaming] +* xref:Synchronization_2_Frame_Pacing.adoc[Synchronization 2 & Frame Pacing] +* xref:Robustness2.adoc[VK_EXT_robustness2] +* xref:Dynamic_Rendering_Local_Read.adoc[Dynamic Rendering Local Read] +* xref:Shader_Tile_Image.adoc[Shader Tile Image] + +link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Culling.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Culling.adoc new file mode 100644 index 00000000..b07815ca --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Culling.adoc @@ -0,0 +1,46 @@ += Frustum Culling and Distance‑based LOD + +Culling is the simplest way to keep your GPU focused on what the camera can see. In this engine we keep it intentionally pragmatic: CPU frustum tests plus a tiny “distance/size LOD” that skips objects that would contribute only a handful of pixels. + +* CPU frustum culling against per‑mesh AABBs +* A tiny distance/size LOD that skips very small objects (projected size threshold) + +== What we do + +1. Extract the camera frustum planes from `proj * view` once per frame. +2. For each mesh instance, transform its local AABB to world space and test against the planes. +3. If enabled, estimate projected pixel size and skip objects below a threshold (separate thresholds for opaque vs transparent). + +== Where to look in the code + +* Plane extraction and AABB tests: +** `renderer_rendering.cpp` (helpers near the top of the file) +* Per-frame culling application: +** `renderer_rendering.cpp` (the render list building and per-pass filtering) +* UI controls: +** ImGui panel in `renderer_rendering.cpp` — “Frustum culling”, “Distance LOD”, and per-pass thresholds + +== Why it’s set up this way + +* AABBs are cheap to transform and test; doing this on the CPU avoids sending obviously invisible draws. +* A projected‑size cutoff is a practical alternative to a full LOD system for large scenes. + +== Tuning tips + +* Start conservative (smaller thresholds), then increase until you can’t notice pop‑in while moving. +* Transparent objects typically need a slightly higher threshold due to blending artifacts at tiny sizes. + +== Future work ideas + +If you want to push this further: + +* Add per-material or per-layer culling rules (e.g., keep signage readable longer). +* Add hierarchical culling (BVH of AABBs) for very large scenes. +* Add GPU occlusion culling (HZB) once the pipeline grows beyond “readable sample” scale. +* Replace the projected-size heuristic with real mesh LODs (or meshlets). + +== What to read next + +* `Rendering_Pipeline_Overview.adoc` +* `ForwardPlus_Rendering.adoc` +* `Ray_Query_Rendering.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Descriptor_Indexing_UpdateAfterBind.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Descriptor_Indexing_UpdateAfterBind.adoc new file mode 100644 index 00000000..c3503d3a --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Descriptor_Indexing_UpdateAfterBind.adoc @@ -0,0 +1,73 @@ += Descriptor Indexing and Stable Descriptor Updates + +Vulkan descriptors are powerful, but they’re also one of the easiest places to accidentally violate “frame in flight” lifetime rules. + +In this engine we use one simple rule: + +*Only update descriptors at a known safe point.* + +That rule keeps streaming stable, keeps validation clean, and (most importantly) keeps the code readable. + +== The safe point + +Each frame‑in‑flight has a fence. At the start of a new frame, we wait for that fence. Once it signals, the GPU is done with any work that referenced this frame’s descriptor sets. That’s the safe moment to update this frame’s sets. + +Why it matters: updating a set that’s still in use leads to invalid writes or so‑called “update‑after‑bind” violations unless you deliberately opt into those behaviors and structure your pipeline around them. The safe point pattern stays portable and clear. + +== What we update + +* Material textures that finished streaming. +* The reflection texture binding (binding 10) for planar reflections. +* Per‑frame buffers for Forward+ (tile headers/indices, lights SSBO) when resized. + +In Ray Query mode we also refresh the large texture table (the fixed-size sampler array) so that newly streamed textures become visible without rebuilding the pipeline. + +We refresh only the current frame’s sets at the safe point and leave other frames to update at their own turn. This prevents cross‑frame “flip‑flop” where a texture looks different on alternating frames. + +== Descriptor Indexing: when to use it + +Descriptor Indexing opens features such as variable‑sized arrays and update‑after‑bind. It’s powerful, but it shifts complexity to your synchronization and lifetime rules. In this sample we emphasize clarity: + +* We keep descriptor layouts simple and stable. +* We update at the safe point rather than while a command buffer might still be pending. + +When we do use descriptor indexing features, it’s for one specific reason: large, non-uniformly indexed descriptor arrays (e.g., Ray Query’s texture table). In that case, correctness depends on: + +* enabling the descriptor indexing feature bits required by the GPU +* marking bindings with the correct binding flags (when supported) +* never caching stale Vulkan image/sampler handles across async streaming + +If your project needs truly dynamic descriptor arrays or frequent mid‑frame updates, Descriptor Indexing can be the right tool—just document the new invariants carefully. + +== Practical tips + +* Centralize descriptor updates; don’t scatter writes across the frame. +* Use default textures for placeholders, then swap once—don’t bounce between real and default. +* Prefer combined image samplers for samples aimed at teaching; split image/sampler only when you need the flexibility. + +== Where to look in the code + +* Frame safe point + per-frame descriptor refresh: +** `renderer_rendering.cpp` +* Descriptor set layouts (including update-after-bind flags when enabled): +** `renderer_pipelines.cpp` +* Device feature enable for descriptor indexing: +** `renderer_core.cpp` +* Streaming-safe Ray Query texture table rebuild: +** `renderer_ray_query.cpp` + +== Future work ideas + +If you want to explore more advanced descriptor patterns: + +* Move to variable descriptor counts for texture tables (when device support is good enough for your targets). +* Use separate image/sampler descriptors to share samplers across many textures. +* Add a “descriptor stress test” mode (development-only) that rapidly streams textures to validate lifetime rules. + +== What to read next + +* `Synchronization_and_Streaming.adoc` +* `Separate_Image_Sampler_Descriptors.adoc` +* `Ray_Query_Rendering.adoc` + +This conservative approach avoids common pitfalls while keeping the code approachable. diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc new file mode 100644 index 00000000..189f8559 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Dynamic_Rendering_Local_Read.adoc @@ -0,0 +1,39 @@ += VK_KHR_dynamic_rendering_local_read — keeping color data in tile memory + +Dynamic Rendering lets you render without full render passes/subpasses. The `VK_KHR_dynamic_rendering_local_read` feature is a small but handy addition: it allows same‑pass reads from attachments via tile/local memory paths on hardware that supports it. + +== Why it matters + +Some post‑lighting effects and resolve‑like steps read the color you just wrote. With this feature, drivers can service those reads from fast on‑chip memory instead of round‑tripping to VRAM. + +== How we approach it + +* We enable the feature if present and keep codepaths compatible when it isn’t. +* We still end a rendering instance before doing layout transitions. The feature does not allow arbitrary barrier misuse — regular Synchronization 2 rules apply. + +== Practical guidance + +* Treat this as an optimization, not a new API surface. +* Keep stage/access masks precise. In this sample we keep transitions outside active rendering for clarity. + +== Where to look in the code + +* Feature detection and enablement: +** `renderer_core.cpp` (device feature enable path) +* Dynamic rendering setup + barriers: +** `renderer_rendering.cpp` +** `renderer_pipelines.cpp` + +== Future work ideas + +If you want to demonstrate local-read more directly: + +* Add a small “same-pass” effect that reads the current color attachment (e.g., a simple local contrast or edge highlight). +* Add a debug HUD that prints whether the feature is enabled on the current device. +* Compare performance with and without local-read on tile-based GPUs (mobile) using a fixed camera path. + +== What to read next + +* `Rendering_Pipeline_Overview.adoc` +* `Synchronization_and_Streaming.adoc` +* `Synchronization_2_Frame_Pacing.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/ForwardPlus_Rendering.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/ForwardPlus_Rendering.adoc new file mode 100644 index 00000000..ab4df93c --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/ForwardPlus_Rendering.adoc @@ -0,0 +1,37 @@ += Forward+ Rendering in this Sample + +Forward+ keeps the forward shading model you already know, but limits the per‑pixel light loop to only the lights that might affect that pixel. It does this by dividing the screen into tiles (and optionally Z‑slices) and building per‑tile light lists with a compute pass. + +== What we do + +* Depth pre‑pass (optional): populates depth so the compute stage can cull by Z more effectively. +* Compute pass: assigns lights to tiles (and slices) and writes compact lists to SSBOs. +* Main PBR pass: for each pixel, fetch the tile header and iterate only those lights. + +== Where to look in code + +* Buffers, per-frame state, and descriptor bindings: +** `renderer_compute.cpp` and `renderer_resources.cpp` (look for the `ForwardPlusPerFrame` data) +* Compute dispatch and per-frame parameters: +** `renderer_rendering.cpp` +* Shader-side light list consumption: +** `shaders/pbr.slang` (Forward+ light loop) + +== Tips + +* Tune tile size; 16×16 is a reasonable default for 1080p. +* If you pre‑pass depth, use `depthWriteEnable=false` and `depthCompare=Equal` in the subsequent opaque color pass. + +== Future work ideas + +If you want to take this beyond a compact sample: + +* Upgrade from 2D tiles to clustered Forward+ (depth slicing and/or logarithmic Z). +* Add a small light “budget” UI and debug visualizations (tile heatmap) behind a development build flag. +* Add shadowing (start with a single directional light shadow map) and extend the tile data to include shadowed light indices. + +== What to read next + +* `Forward_ForwardPlus_Deferred.adoc` +* `Rendering_Pipeline_Overview.adoc` +* `Synchronization_2_Frame_Pacing.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Forward_ForwardPlus_Deferred.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Forward_ForwardPlus_Deferred.adoc new file mode 100644 index 00000000..d8fb4d6a --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Forward_ForwardPlus_Deferred.adoc @@ -0,0 +1,102 @@ += Forward, Forward+, and Deferred — choosing the right path + +Vulkan lets you build many kinds of pipelines. In practice, most real‑time engines gravitate toward one of three shading architectures: Forward, Forward+, or Deferred. + +This page explains what each one is, why this sample chooses Forward+, and where the relevant pieces live in the code. + +== Forward rendering + +Forward draws each object with its lighting in a single pass. It’s the most direct model: bind a material, bind lights (uniforms or textures/SSBOs), draw. It’s easy to reason about and integrates well with transparency and MSAA. + +Pros: + +* Simple and predictable. +* Good with transparent objects and MSAA. +* Great for small light counts or baked lighting. + +Cons: + +* Per‑pixel light loops can get expensive as the number of lights grows. +* You evaluate lights even when most don’t affect the pixel. + +== Forward+ (what we use for dynamic lights) + +Forward+ partitions the screen into tiles and assigns lights to those tiles with a compute pass. The main pass then shades with only the lights relevant to the pixel’s tile. In this sample we use a lightweight Forward+ that focuses on emissive/simplified lights to keep the code approachable. + +Pros: + +* Scales to many local lights; you only evaluate lights that might affect the pixel. +* Keeps forward’s strengths (transparency/MSAA friendliness). + +Cons: + +* Requires a pre‑pass or depth info and a compute dispatch to build the tile lists. +* More moving parts than plain forward. + +== Deferred shading (when to consider it) + +Deferred writes material properties (G‑Buffer) in the first pass, then lights that buffer in a second pass. That turns lighting cost into “cost per lighted pixel” and tends to excel with many lights, but it makes transparency and MSAA trickier. + +Pros: + +* Many dynamic lights at high performance. +* Clear separation of material/write and light/evaluate. + +Cons: + +* Transparent objects must be handled separately (often with a forward pass). +* MSAA is more complex; memory bandwidth can be high. + +== What the sample uses (and why) + +We use Forward+ for small, dynamic lights and a forward material path for everything else. That keeps the code compact while still letting you place many little lights around the scene. Transparency (glass) is shaded in a second forward pass so order and blending are correct. + +If your project needs hundreds of shadowed lights and complex post‑lighting, explore a deferred path or a hybrid: deferred for opaque, forward for transparent. + +== Implementation highlights in this codebase + +* A small compute pass builds per‑tile light lists. +* Per‑frame SSBOs hold tile headers/light indices; the main PBR pass reads those to loop only relevant lights. +* Descriptor updates happen at the frame’s safe point so we don’t touch in‑use sets. + +== Where to look in the code + +* Forward/Forward+ render loop integration: +** `renderer_rendering.cpp` +* Pipeline + descriptor layout setup: +** `renderer_pipelines.cpp` +* Main PBR shader (reads per-tile light lists when Forward+ is enabled): +** `shaders/pbr.slang` + +NOTE: The tile/cluster build shader is wired in `renderer_pipelines.cpp`. Start there and follow which compute pipeline is created for the Forward+ light assignment pass. + +== Choosing for your project + +Use Forward if: + +* Light count is low, transparency/MSAA are priorities, and you want the simplest pipeline. + +Use Forward+ if: + +* You want many local lights but still want forward’s strengths. + +Use Deferred if: + +* You need to scale to many dynamic lights with complex lighting, and you’re ready to solve transparency/MSAA separately. + +There’s no one answer; pick the simplest that meets your needs. You can always grow the pipeline later. + +== Future work ideas + +If you want to expand the lighting system beyond “readable sample”: + +* Add clustered Forward+ (3D clusters using depth slices) instead of 2D tiles. +* Add shadows (start with a single directional shadow map, then add point/spot shadows). +* Add a small deferred path for opaque only (keep transparent as forward). +* Add ray query helpers for selective effects (reflection rays, shadow rays, or AO probes) without building a full RT pipeline. + +== What to read next + +* `Rendering_Pipeline_Overview.adoc` +* `ForwardPlus_Rendering.adoc` +* `Synchronization_2_Frame_Pacing.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/GLTF_Animation.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/GLTF_Animation.adoc new file mode 100644 index 00000000..eb0c4a90 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/GLTF_Animation.adoc @@ -0,0 +1,259 @@ += glTF Animation and Transform Composition + +Animations bring life to 3D scenes — spinning ceiling fans, rotating gears, walking characters. This page explains how this engine loads and plays animations from glTF files, with a strong focus on *transform composition* (the thing that keeps objects animating in place instead of “drifting away”). + +== What are glTF animations? + +glTF (GL Transmission Format) is a standard for 3D assets that includes support for skeletal and node-based animations. Animations in glTF consist of: + +* **Channels**: Define which node property to animate (translation, rotation, or scale) +* **Samplers**: Provide keyframe data and interpolation methods (step, linear, or cubic spline) +* **Timeline**: Time values mapping to output values for smooth playback + +When you export an animated model from Blender, Maya, or other 3D tools, the animation data captures how nodes move over time *relative to their initial transforms*. This is crucial: animations describe *deltas* (changes), not absolute world positions. + +== Understanding transform composition + +Transform composition is the foundation of how animations work in 3D engines. Understanding this concept is essential for implementing any animation system. + +**The core principle**: Animation data in GLTF describes *changes* (deltas), not absolute positions. When an artist animates a ceiling fan spinning in Blender, they're defining how much it rotates over time, not where it should be in world space. + +Consider a ceiling fan at position (10, 5, 8) with an animation that rotates it. The animation keyframes might specify: +* Translation: (0, 0, 0) — no movement +* Rotation: 0° → 360° around the Y axis — spinning +* Scale: (1, 1, 1) — no scaling + +To display this correctly, we must **compose** the animation delta with the object's base transform: + +* **Base transform**: The object's initial position/rotation/scale from the scene hierarchy +* **Animation transform**: The time-varying delta from the keyframes +* **Final transform**: Base composed with Animation + +The composition rules are: +* Translation: `final = base + animDelta` (addition) +* Rotation: `final = base * animDelta` (quaternion multiplication) +* Scale: `final = base * animDelta` (component-wise multiplication) + +With proper composition, our ceiling fan remains at (10, 5, 8) and spins in place. + +== How it works in our engine + +Our animation system has three key components: + +=== 1. Loading: Extract base transforms and animation data + +When loading a glTF file (`model_loader.cpp`), we extract: + +* **Node transforms**: Each GLTF node has a local transform matrix stored in `animatedNodeTransforms` map +* **Animation data**: Channels, samplers, and keyframes stored in `Animation` objects +* **Node-to-mesh mapping**: Links node indices to mesh indices for entity matching + +[source,cpp] +---- +// In model_loader.cpp (conceptual) +std::unordered_map animatedNodeTransforms; // nodeIndex -> base transform +std::unordered_map animatedNodeMeshes; // nodeIndex -> meshIndex +std::vector animations; // animation clips +---- + +=== 2. Scene setup: Create entities and apply base transforms + +In `scene_loading.cpp`, for each animated node: + +* **Create separate entities**: If multiple nodes share the same mesh (like two ceiling fans), create individual entities so each can animate independently +* **Apply base transforms**: Decompose the node's transform matrix into position/rotation/scale and set the entity's TransformComponent +* **Build nodeToEntity map**: Links GLTF node indices to entity pointers for animation targeting + +[source,cpp] +---- +// For each animated node +glm::mat4 nodeTransform = animatedNodeTransforms[nodeIndex]; +glm::vec3 position, scale; +glm::quat rotation; +glm::decompose(nodeTransform, scale, rotation, position, ...); + +transform->SetPosition(position); // Base position (e.g., ceiling) +transform->SetRotation(eulerAngles(rotation)); +transform->SetScale(scale); +---- + +**Critical insight**: animated nodes that share geometry must have separate entities. GPU instancing (one entity, multiple transforms) doesn’t work for individual animation control. + +== Where to look in the code + +If you want to follow the data end-to-end: + +* glTF parsing (nodes, animations, samplers): +** `model_loader.cpp` +** `model_loader.h` +* Scene/entity creation and node→entity mapping: +** `scene_loading.cpp` +* Animation playback and transform composition: +** `animation_component.cpp` +** `animation_component.h` +* Transform storage and composition helpers: +** `transform_component.cpp` +** `transform_component.h` + +== Future work ideas + +If you want to grow the animation system: + +* Support animation blending (cross-fade between clips). +* Add skeletal skinning (vertex blending) if you want character animation. +* Add an animation debug UI that shows the active clip/time per entity (development-only). +* Add “bake transforms” options (useful for static meshes that only need a single animated pose). + +== What to read next + +* `Synchronization_and_Streaming.adoc` (animation + streaming can interact in large scenes) +* `Rendering_Pipeline_Overview.adoc` +* `Push_Constants_Per_Object.adoc` + +=== 3. Playback: Compose animation with base transforms + +In `AnimationComponent::Update()`: + +1. **Capture base transforms on first frame**: Store each entity's initial position/rotation/scale when animation starts +2. **Sample keyframes**: Interpolate animation data at current time +3. **Compose transforms**: Add/multiply animation deltas with base transforms +4. **Apply to entity**: Update the TransformComponent with the composed result + +[source,cpp] +---- +// Animation update logic +glm::vec3 basePos = basePositions[nodeIndex]; // e.g., (10, 5, 8) +glm::vec3 animTranslation = SampleVec3(sampler, time); // e.g., (0, 0, 0) +transform->SetPosition(basePos + animTranslation); // Result: (10, 5, 8) + +glm::quat baseRot = baseRotations[nodeIndex]; // e.g., identity quaternion +glm::quat animRotation = SampleQuat(sampler, time); // e.g., 45° around Y +glm::quat finalRotation = baseRot * animRotation; // Compose using quaternion multiplication +transform->SetRotation(glm::eulerAngles(finalRotation)); // Convert to Euler for transform +---- + +== Transform composition rules + +Different transform properties compose differently: + +**Translation**: Additive + +[source] +---- +finalPosition = basePosition + animationTranslation +---- +Addition works naturally for positions in 3D space. + +**Rotation**: Quaternion multiplication + +[source] +---- +finalRotation = baseRotation * animationRotation // quaternion math +finalEuler = eulerAngles(finalRotation) // convert for display +---- +Rotations must be composed using quaternion multiplication to avoid gimbal lock and correctly preserve rotation order. Always work in quaternion space during composition, then convert to Euler angles only when setting the transform. + +**Scale**: Multiplicative + +[source] +---- +finalScale = baseScale * animationScale // component-wise +---- +Animation scale of (1, 1, 1) means "no change", (2, 1, 1) means "double X axis". + +== Handling multiple instances + +When two GLTF nodes reference the same mesh (e.g., two identical ceiling fans), you need separate entities for independent animation. + +**Why separate entities?** +* GPU instancing is designed for many identical, non-animated objects (trees, rocks, grass) +* Instance transforms are set once per frame; you cannot animate each instance independently +* Animation requires per-entity TransformComponents that update every frame + +**Implementation approach**: Create separate entities + +[source,cpp] +---- +// First node reuses existing entity +nodeEntity = geometryEntities[meshIndex]; + +// Second node creates new entity with cloned geometry +nodeEntity = engine->CreateEntity("AnimNode_5"); +mesh->SetVertices(sourceMesh->vertices); // Clone mesh data +mesh->SetIndices(sourceMesh->indices); +---- + +Each entity gets its own TransformComponent and can animate independently. + +== Keyframe interpolation + +GLTF supports three interpolation modes: + +**Step**: Jump instantly to next keyframe (no smoothing) + +[source,cpp] +---- +return keyframe0.value; // Robotic, retro feel +---- + +**Linear**: Smooth linear blend between keyframes + +[source,cpp] +---- +return glm::mix(v0, v1, t); // Most common, looks natural +---- + +**Cubic Spline**: Smooth curves using tangents + +[source,cpp] +---- +// Hermite spline using in-tangent, value, out-tangent +// For production: implement full cubic interpolation for smoother motion +---- + +For rotations, use spherical linear interpolation (slerp) instead of mix: + +[source,cpp] +---- +return glm::slerp(q0, q1, t); // Avoids gimbal lock +---- + +== Performance considerations + +**Animation Update Cost**: O(channels × entities) +* For 10 animated objects with 3 channels each (translation, rotation, scale): ~30 transform updates per frame +* This is cheap; transform math is fast + +**Memory**: Each animated entity needs: +* Cloned mesh data (vertices, indices): ~100KB for a ceiling fan +* Transform storage: 3×vec3 = 36 bytes per node + +**Optimization tip**: If you have hundreds of identical animated objects (e.g., grass blades), consider GPU-side animation with compute shaders instead of per-entity CPU updates. + +== Alternatives and extensions + +**Skeletal animation (skinning)**:: +* For characters with bones/joints +* Requires vertex skinning (blend multiple bone transforms per vertex) +* More complex than node animation but enables realistic deformation + +**Morph targets (blend shapes)**:: +* For facial animation or smooth shape transitions +* GLTF supports weights channel for morph targets +* Extends beyond node transforms to deform mesh vertices + +**Procedural animation**:: +* Generate animation data at runtime (e.g., wind sway, noise-based motion) +* More flexible but requires custom authoring + +== What to read next + +If you want to dive deeper: + +* **Transform Component**: See `transform_component.h` for how we store and compute model matrices +* **GLTF Specification**: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations +* **Synchronization**: How animation updates interact with render frame timing + +The key takeaway: **Always compose animation transforms with base transforms**. This fundamental principle is what makes objects animate in their correct world positions while the animation data itself describes relative changes. Understanding this composition is essential for any animation system. + +Now you have the foundation to implement GLTF animations in your own projects. Happy animating! 🚁 diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Mipmaps_and_LOD.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Mipmaps_and_LOD.adoc new file mode 100644 index 00000000..c7b4ea00 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Mipmaps_and_LOD.adoc @@ -0,0 +1,39 @@ += Mipmaps and Level of Detail (LOD) + +Mipmaps reduce aliasing and bandwidth by sampling pre‑filtered versions of a texture. LOD is simply “which mip do we use right now?”. + +In this sample the key idea is: we want stable, good-looking texture sampling while assets are streaming in, without turning texture management into a giant subsystem. + +== What we do here + +* Use mipmapped textures when available (KTX2 transcodes can include mips). +* For raw RGBA uploads, we cap auto‑generated mips to a small number to avoid large VRAM spikes. +* Enable sampler anisotropy with a UI slider so you can see the trade‑offs quickly. + +== Where it lives in code + +* Sampler creation and anisotropy slider: +** `renderer_resources.cpp` (sampler creation helpers) +** ImGui panel in `renderer_rendering.cpp` +* Upload path (staging → device image, then transition to `SHADER_READ_ONLY_OPTIMAL`): +** `renderer_resources.cpp` +** `resource_manager.cpp` / `scene_loading.cpp` (higher-level streaming/control flow) + +== Tips + +* Prefer compressed formats (BC/ASTC/ETC) with mips for big scenes. +* Clamp the max anisotropy to what your device supports. + +== Future work ideas + +If you want to take this farther: + +* Add a per-material “mip bias” control (great for stylized looks and debugging shimmering). +* Add texture streaming by mip level (load low mips first, then refine). +* Add a small “texture residency” overlay (counts of textures by mip availability). + +== What to read next + +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Synchronization_and_Streaming.adoc` +* `Ray_Query_Rendering.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Planar_Reflections.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Planar_Reflections.adoc new file mode 100644 index 00000000..7c251344 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Planar_Reflections.adoc @@ -0,0 +1,160 @@ += Planar Reflections in Our Engine + +You’ve probably noticed shiny floors and windows in real‑world scenes. In games, we often fake that look. In this engine we chose a practical, reliable technique: planar reflections. This page explains what they are, why we use them, how they’re implemented, and when you might want something else. + +== What are planar reflections? + +Planar reflections render a mirror image of the scene across a single plane (e.g., a flat floor or a window). Think of it as a “mirror camera” that looks into the scene from the other side of the reflective surface. We render that mirrored view into a texture, then sample that texture when drawing glass (or any reflective planar surface). + +Planar reflections work great for: + +* Flat mirrors, calm water, polished floors, glass panes. +* Scenes where you need stable, high‑quality reflections without heavy noise or temporal instability. + +They are not ideal for: + +* Curved/rough surfaces that need glossy, view‑dependent blurs everywhere. +* Arbitrary reflection directions (e.g., metals with complex micro‑geometry). + +== Why we chose planar reflections for the sample + +We want a reflection method that is: + +* Easy to understand (one extra pass, one extra texture). +* Deterministic and stable (no “sparkles” or temporal accumulation headaches). +* Practical for a single dominant reflector (glass, floor) in a forward renderer. + +Planar reflections deliver all three. They also scale well across GPUs without requiring ray tracing hardware. + +== How it works in our engine + +We add one small pass and one small blend in the main pass: + +1. Mirror pass (off‑screen) +** Compute a mirrored view matrix by reflecting the camera across a plane (e.g., Y=0 for a ground plane). +** Render the opaque scene with face culling disabled (or adjusted) into a reflection color+depth target. +** Synchronize the reflection image for sampling in the next pass. + +2. Main pass (normal camera) +** Draw opaque + transparent objects as usual. +** When drawing glass, sample the reflection texture and blend it with glass shading using Fresnel + roughness + a user‑controlled “reflection intensity”. + +That’s it. No special render graph magic, no ray queries, no temporal accumulation. + +== Where to look in the code + +* Mirror camera math, reflection pass, and pass ordering: +** `renderer_rendering.cpp` +* Reflection render targets and pipeline setup: +** `renderer_pipelines.cpp` +* Reflection sampling + glass shading: +** `shaders/pbr.slang` +** `shaders/pbr_utils.slang` +* Reflection binding and per-frame safe-point updates: +** `renderer_rendering.cpp` (reflection descriptor refresh) + +== The mirror math (short and sweet) + +You define a plane in world space: `ax + by + cz + d = 0`. + +From that plane you build a reflection matrix `R`. Apply `R` to the regular camera view to get the mirrored view. In practice you’ll also flip culling or set `cullMode = none` for the mirrored pass because the winding order changes under reflection. + +We also pass the plane to shaders for optional clipping: + +* A simple dot(product) with world position lets us discard fragments “behind” the plane in the mirror pass. + +== The rendering steps in detail + +Mirror pass: + +* Create a reflection color image (format matches your main pass needs; we pick a color format that the composite/glass pass can sample easily) and a reflection depth image. +* Before rendering: transition the reflection color image from `SHADER_READ_ONLY_OPTIMAL` to `COLOR_ATTACHMENT_OPTIMAL` using Synchronization 2 (`vkCmdPipelineBarrier2`). Do the same for depth to `DEPTH_ATTACHMENT_OPTIMAL`. +* Begin dynamic rendering, bind the PBR pipeline for opaque objects, and disable culling (or flip front faces). +* Render opaque meshes. You can add a clip test against the plane if needed. +* End rendering. Transition the reflection color image to `SHADER_READ_ONLY_OPTIMAL` for sampling in the main pass. + +Main pass: + +* Render opaque as usual (we use an off‑screen buffer to do tone‑mapped composite later). +* Transparent pass: when drawing glass, sample the reflection texture and blend: +** Use Fresnel (stronger at grazing angles) and reduce with roughness. +** Multiply by a small “reflection intensity” you can tune in the UI. + +== Synchronization and barriers (what matters) + +We keep it simple with Vulkan Synchronization 2: + +* Do not change image layouts inside an active dynamic render pass. End it first. +* Use `vkCmdPipelineBarrier2` with: correct source/destination stage masks, access masks, and old/new layouts. +* Reflection color: `SHADER_READ_ONLY_OPTIMAL → COLOR_ATTACHMENT_OPTIMAL` before mirror pass; back to `SHADER_READ_ONLY_OPTIMAL` after. +* Swapchain image: transition to `COLOR_ATTACHMENT_OPTIMAL` for composite/transparent; transition to `PRESENT_SRC_KHR` only after ending the last rendering pass. + +== Descriptors: where is the reflection bound? + +* We reserve binding 10 in the PBR set for the reflection sampler. +* At the per‑frame “safe point” (when previous frame’s work is done), we refresh binding 10 for the current frame to point to the reflection image from the previous frame. +* The glass shader checks a UBO flag (`reflectionEnabled`) and samples only when a valid reflection image exists. + +== Glass blending: an approachable model + +Glass is mostly transmission, but we want vivid, plausible reflections. We use: + +* Fresnel term (Schlick): stronger reflections at grazing angles. +* Roughness factor: more roughness → weaker, blurrier reflections (we keep it simple here and just dim the strength). +* Reflection intensity slider: exposed in the UI so you can tune visibility in seconds. + +This is not a full physical spectral model, and that’s fine. It’s readable and produces convincing results. + +== Alternatives and when to choose them + +Screen‑space reflections (SSR):: +* Works without extra passes; uses existing color/depth from your frame. +* Great for puddles and local effects, but can miss off‑screen objects and suffers from temporal instability. +* Choose SSR if you want quick reflections everywhere and can accept occasional artifacts. + +Environment maps / cube maps / reflection probes:: +* Very fast; precomputed. +* Not view‑accurate for nearby objects; best for distant glossy reflections. +* Choose probes for general ambient reflections or when the surface isn’t a perfect mirror. + +Ray tracing (hardware) / hybrid approaches:: +* Very accurate; supports complex reflections. +* Requires hardware and advanced denoising; more code and performance cost. +* Choose RT if you target high‑end GPUs and want “it just looks right” reflections everywhere. + +Planar reflections (this sample):: +* A single extra pass, deterministic and stable. +* Perfect for one or a few large planar reflectors (floor, windows, calm water). +* Choose this when you want high‑quality mirrors for specific surfaces without adopting ray tracing. + +== Performance tips + +* Render the mirror pass at a lower resolution (we provide a resolution scale slider). +* Cull aggressively (our CPU frustum culling works for both camera and mirrored camera). +* Disable the mirror pass when the reflective surface isn’t visible. +* Consider blurring the reflection sample for rough surfaces if you want softer looks. + +== Troubleshooting + +“Reflections appear too weak”:: +* Increase the Reflection intensity slider and/or reduce roughness. + +== Future work ideas + +If you want to push planar reflections further: + +* Add a roughness-aware blur of the reflection texture (mip chain or separable blur). +* Add multiple reflection planes (useful for multi-floor scenes). +* Add a screen-space fallback (SSR) and blend with planar where valid. +* Add selective ray query reflections for non-planar surfaces (hybrid approach). + +== What to read next + +If you’re curious about the rest of this sample: + +* link:Synchronization_and_Streaming.adoc[Synchronization and Streaming] +* link:ForwardPlus_Rendering.adoc[Forward+ Rendering] +* link:Descriptor_Indexing_UpdateAfterBind.adoc[Descriptor Indexing and Stable Descriptor Updates] +* link:Rendering_Pipeline_Overview.adoc[Rendering Pipeline Overview] + +Enjoy experimenting. This approach is intentionally straightforward so you can focus on learning Vulkan’s moving parts without getting lost in a maze of techniques. diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Push_Constants_Per_Object.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Push_Constants_Per_Object.adoc new file mode 100644 index 00000000..d5b35335 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Push_Constants_Per_Object.adoc @@ -0,0 +1,39 @@ += Push Constants — per‑object material properties + +Push constants are tiny pieces of data you can send to shaders without creating or updating buffers. They’re perfect for per‑draw material knobs. + +In this engine we use push constants for *values that change per draw call* (material factors and “is there a texture?” flags). Anything that changes less frequently (camera data, light lists, big arrays) stays in UBOs/SSBOs. + +== How we use them + +We pack the common PBR controls (base color factor, metallic/roughness, texture presence flags, emissive strength, transmission, IOR) into a single push constant block and update it before each draw. + +== Where to look + +* C++ push constant struct and update call: +** `renderer.h` (`MaterialProperties`) +** `renderer_rendering.cpp` (where we push per-draw material properties) +* Shader push constant block: +** `shaders/pbr.slang` (`[[vk::push_constant]]` block) +* PBR helper functions used by the shader: +** `shaders/pbr_utils.slang` +** `shaders/lighting_utils.slang` + +== Guidelines + +* Keep the block small (Vulkan guarantees at least 128 bytes). This sample fits comfortably. +* Use push constants for values that change every draw call. Use UBO/SSBO for larger, less‑frequent data. + +== Future work ideas + +If you want to extend this pattern: + +* Split “rarely changing per-material data” into a GPU material buffer and use push constants only for the per-draw index. +* Add a second push constant block for per-draw debug visualizations (development-only) to keep it out of hot UBO paths. +* Add a material override system (force roughness/metallic for entire scene) by layering a global UBO on top. + +== What to read next + +* `Rendering_Pipeline_Overview.adoc` +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Ray_Query_Rendering.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Reflections_and_Transparency.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Reflections_and_Transparency.adoc new file mode 100644 index 00000000..642ea309 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Reflections_and_Transparency.adoc @@ -0,0 +1,87 @@ +== Ray Query Reflections and Transparency + +Ray queries make it straightforward to add reflection and refraction to a renderer without adopting a full ray tracing pipeline. In this engine, the Ray Query mode compute shader already computes primary visibility; we extend that shader with *secondary rays* to handle reflective and transmissive materials. + +This page explains the design in a way you can reuse in your own projects. + +=== Two toggles, one clear mental model + +Ray Query mode exposes two feature toggles: + +* *Reflections*: enables a reflection ray from the first hit. +* *Transparency/Refraction*: enables a refraction ray for transmissive materials. + +There’s also a small quality knob: + +* *Max secondary bounces*: `0` disables secondary rays entirely; `1` enables a single bounce. + +The point of the bounce cap is to keep performance predictable while still demonstrating how ray queries can be layered into a physically-based shading model. + +=== Reflection rays (one bounce) + +At the first surface hit we have: + +* the outgoing view direction `V` +* the surface normal `N` +* material parameters (roughness, metallic, and Fresnel base reflectance) + +The reflection direction is the standard geometric reflection: + +`R = reflect(-V, N)` + +In the compute shader we trace a new ray from a small offset along the normal to avoid self-intersections: + +* origin: `P + N * eps` +* direction: `R` + +If the reflection ray hits something, we shade that hit using the same PBR path as the primary ray. If it misses, we use a stable sky/background function. + +The final reflection contribution is weighted by Fresnel and reduced by roughness: + +* grazing angles reflect more +* rough surfaces reflect less strongly + +This keeps the result intuitive and stable. + +=== Thin-glass refraction (one bounce) + +For transmissive materials we implement a “thin glass” model: + +* a refraction ray gives you the view *through* the surface +* a reflection ray gives you the view *on* the surface +* Fresnel blends between them + +We compute refraction using Snell’s law with a simple total internal reflection fallback. + +The refraction ray uses: + +* origin: `P + refrDir * eps` (offset along the refraction direction) +* direction: `refrDir` + +The transmitted result is blended with reflection using Fresnel, and then mixed into the base surface color using the material’s transmission factor. + +=== Alpha-masked surfaces (foliage) + +Many real scenes use *alpha masking* for foliage and thin geometry. Alpha masking is different from regular blending: + +* the surface is either present or absent per pixel +* the decision is driven by a baseColor alpha texture and an `alphaCutoff` + +In a traditional ray tracing pipeline, alpha masking is often implemented in an any-hit shader. With ray queries, we can implement the same idea by controlling which candidate intersections get committed. + +The approach is: + +1. Allow non-opaque candidates for alpha-masked instances. +2. For each candidate triangle hit: + * compute the candidate UV + * sample baseColor alpha + * accept the candidate only when `alpha >= alphaCutoff` + +This produces correct visibility for masked geometry in primary rays, and it also keeps reflections/refractions consistent because they use the same traversal routine. + +=== Where to look in the code + +* Ray Query shader implementation: +** `shaders/ray_query.slang` +* Ray Query UI toggles and bounce cap: +** `renderer_rendering.cpp` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Rendering.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Rendering.adoc new file mode 100644 index 00000000..e9dfd97a --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Ray_Query_Rendering.adoc @@ -0,0 +1,93 @@ +== Ray Query Rendering + +This engine includes a ray-traced rendering mode built on Vulkan’s *ray queries*. Instead of building a full ray tracing pipeline (raygen / miss / hit shaders), ray queries let you perform intersection tests directly from regular shaders. + +In this sample we use ray queries from a *compute shader* to render the whole frame: + +* Build *BLAS* (per mesh) and a *TLAS* (scene instances). +* Dispatch a compute shader that: +** Generates one primary ray per pixel from the camera. +** Uses `TraceRayInline()` to find intersections in the TLAS. +** Shades the hit using the same PBR utilities as the raster path. +* Write the result into a storage image, then composite to the swapchain. + +=== Why ray queries? + +Ray queries are a good fit for a “hybrid” renderer: + +* You can call them from compute, fragment, or other shader stages. +* They reuse the standard descriptor system. +* They keep control flow in your shader code: you decide how to traverse, when to accept hits, and how to shade. + +=== High-level architecture + +At a high level, Ray Query mode touches three areas: + +* *Acceleration structures*: built from the scene’s vertex and index buffers. +* *Descriptors*: bind the TLAS, the output storage image, and the scene data needed for shading. +* *Shader*: generate rays, do the query, shade the hit. + +The important idea is that the ray query shader does not “own” the scene. It reads the same scene assets as rasterization (meshes, materials, textures), but through a separate descriptor set designed for the compute path. + +=== Acceleration structure build (BLAS/TLAS) + +We build acceleration structures once the scene is ready: + +* A BLAS is created per unique mesh. +* Each scene instance is added to the TLAS with its transform. +* Each TLAS instance encodes a custom instance index so the shader can index into a matching `GeometryInfo` table. + +The Ray Query shader uses that per-instance index to look up: + +* device addresses for vertex and index buffers +* the material index +* a per-instance normal transform for correct world-space normals + +=== Descriptor layout + +Ray Query mode uses a dedicated descriptor set layout. The exact binding numbers matter because they must match the shader. + +Typical bindings in this engine are: + +* Binding 0: a small Ray Query-specific UBO (camera matrices, exposure/gamma, toggles) +* Binding 1: the TLAS +* Binding 2: output storage image +* Binding 3: light buffer +* Binding 4: `GeometryInfo` buffer +* Binding 5: material buffer +* Binding 6: a large combined image sampler array used as a texture table + +=== Streaming-safe texture access + +This engine streams textures asynchronously. A key design choice for Ray Query mode is that the shader indexes textures through a fixed-size array (a “texture table”). + +At runtime: + +* Materials store texture *indices* into the table (baseColor, normal, metallic-roughness, occlusion, emissive). +* The renderer refreshes the table using the *current* texture handles. +* Slots `0..4` are reserved for shared default textures (so sampling always has a valid fallback). + +This approach keeps shading simple in the shader: sampling uses `NonUniformResourceIndex()` and `SampleLevel(..., 0.0)` (explicit LOD is important for compute). + +=== Dispatch and presenting the result + +The Ray Query compute shader writes to a storage image (typically HDR-capable). + +After dispatch: + +* A barrier transitions the Ray Query output image from `GENERAL` (write) to `SHADER_READ_ONLY_OPTIMAL` (read). +* A fullscreen composite pass samples the output image and writes to the swapchain. +* A final transition prepares the swapchain for present. + +This lets the engine reuse the same post-processing controls (exposure/gamma) for both raster and ray query paths. + +=== Where to look in the code + +* Shader: +** `shaders/ray_query.slang` +* CPU-side Ray Query build and descriptors: +** `renderer_ray_query.cpp` +* Render loop integration + UI: +** `renderer_rendering.cpp` +* Descriptor indexing features (for large sampler arrays): +** `renderer_core.cpp` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Rendering_Pipeline_Overview.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Rendering_Pipeline_Overview.adoc new file mode 100644 index 00000000..206adbd5 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Rendering_Pipeline_Overview.adoc @@ -0,0 +1,56 @@ += Rendering Pipeline Overview (this sample) + +This engine uses Vulkan Dynamic Rendering with a small, readable sequence of passes. The order is deliberate: it keeps tone mapping explicit, keeps transparency sane, and makes it easy to plug in optional features like planar reflections or the Ray Query compute path. + +== The pass order + +1. Optional reflection pass (off‑screen): + * Mirror the camera across a plane (e.g., floors or windows). + * Render opaque geometry into a reflection render target (color + depth). + * Transition the reflection image to `SHADER_READ_ONLY_OPTIMAL` for the next frame. + +2. Opaque to off‑screen color: + * Render all opaque objects into an off‑screen color image (`opaqueSceneColor`). + * Depth is read/write as usual (or read‑only if you ran a depth pre‑pass). + +3. Composite to swapchain: + * End the opaque rendering. + * Transition `opaqueSceneColor` to `SHADER_READ_ONLY_OPTIMAL`. + * Begin a new rendering instance targeting the swapchain image and draw a full‑screen pass that samples the off‑screen color (tone mapping included). + +4. Transparent on top: + * Keep the swapchain image as the color attachment and bind the scene depth. + * Render transparent objects (glass/liquids) back‑to‑front. + * Glass samples the prior frame’s reflection texture when enabled. + +5. UI: + * Render the UI on top. + * Transition the swapchain image to `PRESENT_SRC_KHR` after ending rendering. + +== Where to look in the code + +* Main render loop + pass ordering: +** `renderer_rendering.cpp` +* Pipeline setup (dynamic rendering attachments, layouts, formats): +** `renderer_pipelines.cpp` +* Composite pass shader (tone mapping + presentation): +** `shaders/composite.slang` +* PBR shading utilities shared by multiple pipelines: +** `shaders/pbr.slang` +** `shaders/pbr_utils.slang` +** `shaders/lighting_utils.slang` + +== Why this shape + +* A single off‑screen buffer makes tone mapping explicit and avoids gamma‑incorrect copy paths. +* Transparent ordering stays simple because the swapchain is the current color attachment in that pass. +* The reflection pass is optional and self‑contained. + +== Future work ideas + +If you want to take this pipeline further: + +* Add a depth pre-pass for heavy scenes (helps early-z and enables more accurate Forward+ clustering). +* Add a lightweight bloom chain that runs between the off-screen opaque pass and the composite. +* Add a dedicated transparent resolve path (weighted blended OIT) if you need lots of overlapping glass. +* Demonstrate hybrid rendering by calling ray queries from a raster shader (reflection probe, shadow test, or glass-only reflections). diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc new file mode 100644 index 00000000..5ab9e491 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Robustness2.adoc @@ -0,0 +1,51 @@ += VK_EXT_robustness2 — safer defaults for real‑world engines + +Vulkan lets you run fast and close to the metal. That also means a bad index or out‑of‑range access can produce undefined results. `VK_EXT_robustness2` tightens that up so mistakes fail predictably instead of corrupting memory or producing flicker. + +== What it gives you + +* Robust buffer access 2 — out‑of‑bounds buffer reads return zero; writes are discarded. +* Robust image access 2 — out‑of‑range image coordinates clamp or return zero per the spec. +* Null descriptors — a descriptor can be left “null” and the shader sees a defined zero value instead of UB. + +These behaviors make the engine more forgiving while students iterate and while textures stream in. + +== How we use it here + +* We enable the extension and feature structs during device creation when available. +* Shaders are written assuming legal ranges, but if a streaming texture or optional binding is temporarily missing, sampling a null descriptor is defined and safe. +* The Forward+/reflection paths avoid mid‑frame descriptor edits; robustness2 then acts as an extra safety net. + +== When to enable + +Always enable when the device supports it for teaching samples and tools. For shipping titles, you can still keep it on; the performance cost is generally negligible on modern drivers, and the safety is worth it. + +== Where to look in the code + +* Device extension/feature enable: +** `renderer_core.cpp` +** `vulkan_device.cpp` +* Bounds checks and defensive indexing in the Ray Query shader: +** `shaders/ray_query.slang` (bounds checks for `geometryInfoCount` / `materialCount`) +* Safe descriptor update patterns (so you don’t rely on robustness for correctness): +** `renderer_rendering.cpp` (per-frame safe point) +** `Descriptor_Indexing_UpdateAfterBind.adoc` + +== Takeaways + +* Robustness doesn’t replace good synchronization and lifetime rules; it complements them. +* Null descriptors and “safe zero” reads make streaming and feature toggles less fragile. + +== Future work ideas + +If you want to stress-test robustness (without turning the engine into a debugging tool): + +* Add a development-only “fault injection” toggle that intentionally feeds out-of-range indices in a controlled shader path. +* Add a small runtime report that prints whether `VK_EXT_robustness2` is enabled on the current device. +* Add a unit-style GPU test scene that exercises missing textures / missing buffers while keeping VVL clean. + +== What to read next + +* `Synchronization_and_Streaming.adoc` +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Ray_Query_Rendering.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Separate_Image_Sampler_Descriptors.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Separate_Image_Sampler_Descriptors.adoc new file mode 100644 index 00000000..fe1d5a44 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Separate_Image_Sampler_Descriptors.adoc @@ -0,0 +1,42 @@ += Separate Image and Sampler Descriptors — when and why + +Vulkan lets you bind an image view and a sampler either together (combined image sampler) or separately. Combined bindings are simpler to teach and maintain. Separate bindings give you flexibility (e.g., reuse one sampler across many images; change only the sampler states). + +In this sample we default to combined image samplers because they keep the descriptor model simple while we’re focused on bigger engine concepts (streaming, synchronization, pass structure). + +== Our default + +We prefer combined image samplers in this sample for readability and because most materials don’t swap samplers at runtime. + +== When to split + +* You want to toggle sampler states (e.g., enable/disable anisotropy) across many textures without updating every descriptor. +* You have a library of sampler objects (point/linear/aniso/wrap/clamp) and want to mix‑and‑match with images. + +== Practical guidance + +* Keep layouts small and stable for teaching. +* If you introduce split bindings, document lifetime rules clearly: images and samplers can now change independently. + +== Where to look in the code + +* Texture and sampler creation: +** `renderer_resources.cpp` +* Descriptor layouts and bindings: +** `renderer_pipelines.cpp` +* Descriptor updates at the per-frame safe point: +** `renderer_rendering.cpp` + +== Future work ideas + +If you want to demonstrate separate image/sampler descriptors concretely: + +* Create a small sampler “library” (point/linear/aniso, wrap/clamp) and switch sampler indices from the UI. +* Use shared samplers with a large texture table to reduce descriptor update volume during streaming. +* Add per-material sampler selection (e.g., nearest for pixel art signage). + +== What to read next + +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Mipmaps_and_LOD.adoc` +* `Synchronization_and_Streaming.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Shader_Tile_Image.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Shader_Tile_Image.adoc new file mode 100644 index 00000000..8308e9b8 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Shader_Tile_Image.adoc @@ -0,0 +1,40 @@ += VK_EXT_shader_tile_image — fast access to tile data + +Some GPUs expose “tile” or “subpass” data paths that let shaders read from on‑chip color/depth without a round trip to memory. `VK_EXT_shader_tile_image` is a portable way to tap into that. + +In this sample we treat it as an optional optimization: the pipeline remains correct without it, and we only take a “fast path” when the device advertises support. + +== What problem it solves + +Post‑lighting texture reads from the just‑written color can be expensive. With tile image access, certain patterns become cheaper and more deterministic on supported hardware. + +== How we handle it in this sample + +* We enable the feature if present and expose a boolean you can check in code. +* The renderer still uses clean Synchronization 2 barriers and ends dynamic rendering before formal layout transitions. That keeps the code understandable on devices that don’t support tile reads. + +== Guidance + +Use it as an optimization. Write code that’s correct everywhere, then add tile‑image fast paths when available. + +== Where to look in the code + +* Feature detection and enablement: +** `renderer_core.cpp` +* Dynamic rendering setup and attachment transitions (kept explicit for clarity): +** `renderer_rendering.cpp` +** `renderer_pipelines.cpp` + +== Future work ideas + +If you want to demonstrate tile-image usage more directly: + +* Add a small “local read” post effect that reads from the current color attachment and compares with a regular sampled path. +* Add a device capability print (development-only) so students can see when the fast path is active. +* Add a micro-benchmark scene and compare bandwidth on tile-based GPUs. + +== What to read next + +* `Dynamic_Rendering_Local_Read.adoc` +* `Synchronization_2_Frame_Pacing.adoc` +* `Rendering_Pipeline_Overview.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_2_Frame_Pacing.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_2_Frame_Pacing.adoc new file mode 100644 index 00000000..710d8e70 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_2_Frame_Pacing.adoc @@ -0,0 +1,65 @@ += Synchronization 2 and frame pacing in this engine + +Vulkan Synchronization 2 makes barriers and submissions easier to read. This sample uses it to keep uploads and rendering in step without stalls. + +The goal here isn’t “maximum cleverness.” It’s predictable ordering: + +* the transfer queue moves data onto the GPU +* the graphics queue draws using whatever is ready +* the CPU only mutates per-frame resources when it knows the GPU is done with them + +== The moving parts + +* Timeline semaphore on the transfer queue — batches of texture uploads signal increasing values. +* Graphics submit waits on the latest uploads value — by the time we draw, textures are ready to sample. +* Frame fences — each frame‑in‑flight has a fence we wait on at the start of the next frame’s CPU work. + +== Barriers we rely on + +Uploads path: + +* `UNDEFINED → TRANSFER_DST_OPTIMAL` (dstStage: TRANSFER, dstAccess: TRANSFER_WRITE) +* After copy: `TRANSFER_DST_OPTIMAL → SHADER_READ_ONLY_OPTIMAL` (srcStage: TRANSFER, dstStage: FRAGMENT_SHADER) + +Render path: + +* Attachment images transition outside active dynamic rendering blocks using `vkCmdPipelineBarrier2`. +* Swapchain transitions: to `COLOR_ATTACHMENT_OPTIMAL` before composite/transparent, to `PRESENT_SRC_KHR` after ending the last rendering pass. + +== Descriptor updates at the safe point + +At the start of a frame, after waiting on the frame fence, we refresh only this frame’s descriptor sets. That avoids “update‑after‑bind” pitfalls and frame‑to‑frame flicker during streaming. + +== Takeaways + +* Keep transitions outside active `beginRendering`/`endRendering` scopes. +* Use clear stage/access pairs; prefer Synchronization 2 for readability. +* Pair timeline semaphores with fences: timelines coordinate queues; fences bound the CPU turn. + +== Where to look in the code + +* Upload submission and timeline semaphore signaling: +** `renderer_resources.cpp` +** `renderer_utils.cpp` +* Graphics submit waits (including “latest upload value”): +** `renderer_rendering.cpp` +* Image barriers for the render path (attachments + swapchain): +** `renderer_rendering.cpp` +* Swapchain and present integration: +** `swap_chain.h` +** `renderer_rendering.cpp` + +== Future work ideas + +If you want to experiment with pacing and latency: + +* Add a UI toggle for the frames-in-flight count and measure input latency vs throughput. +* Add a “fixed camera path” mode (development-only) to produce repeatable GPU timing comparisons. +* Add GPU timestamp queries around the big passes to visualize where time goes. +* Add async compute experiments (if your device supports it) for things like Forward+ light list building. + +== What to read next + +* `Synchronization_and_Streaming.adoc` +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Rendering_Pipeline_Overview.adoc` diff --git a/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_and_Streaming.adoc b/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_and_Streaming.adoc new file mode 100644 index 00000000..c8b1fcf7 --- /dev/null +++ b/en/Building_a_Simple_Engine/Advanced_Topics/Synchronization_and_Streaming.adoc @@ -0,0 +1,91 @@ += Synchronization and Streaming + +Modern Vulkan gives us powerful tools to keep the GPU busy while assets stream in. This engine uses a background uploader, a dedicated transfer queue, and Synchronization 2 to avoid stalls and flicker. Let’s walk through the moving parts and how they fit together. + +== The idea + +- File I/O and staging happen off the render thread. +- GPU copies and layout transitions run on a transfer queue, not the graphics queue. +- A timeline semaphore lets graphics wait for “the latest finished upload” without micro‑managing per‑resource fences. +- We only update descriptors at a safe point (right after waiting for the in‑flight frame’s fence) so we never write into sets that the GPU is still using. + +This keeps the frame loop simple and responsive—even while large textures stream in. + +== The background uploader + +We enqueue texture jobs (transcode/IO → staging buffer → device image). A dedicated thread: + +1. Batches pending copies into a command buffer on the transfer queue. +2. Records layout transitions from `TRANSFER_DST_OPTIMAL` to `SHADER_READ_ONLY_OPTIMAL` using Synchronization 2. +3. Submits once, signaling a monotonically increasing timeline value. +4. Notifies the renderer which textures are now “ready to sample.” + +The render submit includes a wait on the latest uploads timeline value, so textures are available by the time we draw. + +== The safe point for descriptor updates + +Vulkan won’t let us mutate a descriptor set that’s currently in use. The engine does this instead: + +* At the start of each frame, we wait for the fence associated with that frame‑in‑flight. +* Now it’s safe to update this frame’s descriptor sets (they aren’t in use). +* We refresh image bindings with the uploaded texture’s view/sampler at this point. + +As a result there’s no texture “flip‑flop” or flicker: once a real texture replaces a placeholder, it stays. + +== Synchronization 2 in practice + +Uploads path uses `vkCmdPipelineBarrier2` with clear, minimal scopes: + +* Staging → image copy: make the destination image `TRANSFER_DST_OPTIMAL`. +* After the final copy: transition to `SHADER_READ_ONLY_OPTIMAL` (src stage = `eTransfer`, dst stage = `eFragmentShader`). +* Ownership transfers only if the transfer and graphics queues use different families (most desktop drivers share families). + +On the graphics side, we keep attachment layout transitions outside of any dynamic render pass instance and also use `vkCmdPipelineBarrier2` for readability. + +== A typical texture’s journey + +1. Job enqueued with a file path. +2. Background thread: load/transcode to staging, allocate device image, record copies. +3. Submit to transfer queue; signal timeline. +4. Renderer’s next frame begins; the per‑frame fence unblocks. +5. Descriptor for this frame updates to point at the uploaded image (safe point). +6. Draw: fragment shader samples the new texture without stalls. + +== Tips and pitfalls + +* Keep descriptor updates at the safe point. Avoid updating in‑use sets. +* Use the transfer queue for bulk copies; keep the graphics queue focused on drawing. +* Prefer Synchronization 2 for clarity (stage/access pairs are explicit, transitions stand out). +* Batch uploads: the fewer submits, the lower the CPU overhead. + +That’s all it takes to make streaming feel “invisible” to the player—and tidy to maintain. + +== Where to look in the code + +* High-level scene load and job enqueue: +** `scene_loading.cpp` +** `resource_manager.cpp` +* Texture/image creation + upload path: +** `renderer_resources.cpp` +* Transfer queue submission + synchronization helpers: +** `renderer_utils.cpp` +** `vulkan_device.cpp` +* Frame “safe point” (per-frame fence wait) and descriptor refresh: +** `renderer_rendering.cpp` +* Descriptor update patterns (why we update at the safe point): +** `Descriptor_Indexing_UpdateAfterBind.adoc` + +== Future work ideas + +If you want to push streaming further: + +* Stream by mip level (low mips first), then refine in the background. +* Add a small streaming HUD (bytes queued, bytes uploaded, textures ready) behind a development build flag. +* Add per-resource priorities (camera distance, importance tags) so the most noticeable assets arrive first. +* Add “hot reload” for textures to validate descriptor lifetime rules under rapid churn. + +== What to read next + +* `Synchronization_2_Frame_Pacing.adoc` +* `Descriptor_Indexing_UpdateAfterBind.adoc` +* `Ray_Query_Rendering.adoc` diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc index fca97fb2..abf10459 100644 --- a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -189,6 +189,14 @@ private: - Add automated startup probes that dump device/feature info to logs for field telemetry. - Expand the regression scene suite to cover TBR‑sensitive and bandwidth‑heavy paths. +==== Explore Advanced Topics (Simple Engine Tutorials) + +The following short, focused tutorials build directly on the Simple Engine and are great next steps: + +- xref:../Advanced_Topics/01_introduction.adoc[Tutorials Index — browse all topics] +- xref:../Advanced_Topics/Mipmaps_and_LOD.adoc[Mipmaps and LOD] — practical guidance on stable texture sampling and anisotropy. +- xref:../Advanced_Topics/Dynamic_Rendering_Local_Read.adoc[Dynamic Rendering Local Read] — optimize same‑pass reads via tile/local memory when supported. + === Code Examples The complete code for this chapter can be found in the following files: diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc index 16c3b90e..9071755d 100644 --- a/en/Building_a_Simple_Engine/introduction.adoc +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -45,6 +45,8 @@ Let's begin our journey into engine development with these chapters: 7. xref:Tooling/01_introduction.adoc[Tooling] - CI/CD, Debugging, Crash minidump, Distribution, and Vulkan extensions for robustness. 8. xref:Mobile_Development/01_introduction.adoc[Mobile Development] - Adapting the engine for Android/iOS, focusing on performance considerations and mobile-specific Vulkan extensions. +9. xref:Advanced_Topics/01_introduction.adoc[Advanced Topics] - Short, focused tutorials that extend the Simple Engine with specific features and optimizations. + xref:../conclusion.adoc[Previous: Main Tutorial Conclusion] | xref:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] From 786c5cdcea4cc53a4efb7c0b76dca72695eb2b0e Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 17 Dec 2025 00:58:57 -0800 Subject: [PATCH 05/24] Refactor material caching and resource initialization to improve performance, reduce redundant lookups, and enhance thread safety in rendering pipelines. Hopefully fix windows, also, fix frames-in-flight so we can handle more than 1. Hopefully, improve the windows frame rate. In linux testing all rendering types and options result in about 60fps on an older computer. Forward+ does drop to 30, but that's due to requirements of the Forward+ methodology. --- attachments/simple_engine/engine.cpp | 151 ++++- attachments/simple_engine/engine.h | 17 + attachments/simple_engine/renderer.h | 100 ++- attachments/simple_engine/renderer_core.cpp | 22 +- .../simple_engine/renderer_pipelines.cpp | 9 +- .../simple_engine/renderer_ray_query.cpp | 62 +- .../simple_engine/renderer_rendering.cpp | 610 ++++++++++-------- .../simple_engine/renderer_resources.cpp | 260 +++++--- attachments/simple_engine/scene_loading.cpp | 9 +- 9 files changed, 818 insertions(+), 422 deletions(-) diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp index 5a455a75..db0c536f 100644 --- a/attachments/simple_engine/engine.cpp +++ b/attachments/simple_engine/engine.cpp @@ -33,6 +33,37 @@ Engine::Engine() : { } +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(); @@ -46,6 +77,9 @@ bool Engine::Initialize(const std::string &appName, int width, int height, bool // 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)) { @@ -188,8 +222,11 @@ void Engine::Cleanup() } // Clear entities - entities.clear(); - entityMap.clear(); + { + std::unique_lock lk(entitiesMutex); + entities.clear(); + entityMap.clear(); + } // Clean up subsystems in reverse order of creation imguiSystem.reset(); @@ -205,6 +242,7 @@ void Engine::Cleanup() 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); @@ -219,6 +257,7 @@ Entity *Engine::CreateEntity(const std::string &name) Entity *Engine::GetEntity(const std::string &name) { + std::shared_lock lk(entitiesMutex); auto it = entityMap.find(name); if (it != entityMap.end()) { @@ -234,6 +273,17 @@ bool Engine::RemoveEntity(Entity *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(); @@ -271,12 +321,50 @@ bool Engine::RemoveEntity(Entity *entity) bool Engine::RemoveEntity(const std::string &name) { - Entity *entity = GetEntity(name); - if (entity) + // If called from a background thread, defer removal to avoid deleting entities + // while the render thread may be iterating a snapshot. + if (!IsMainThread()) { - return RemoveEntity(entity); + std::lock_guard lk(pendingEntityRemovalsMutex); + pendingEntityRemovalNames.push_back(name); + return true; } - return false; + + 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) @@ -441,6 +529,9 @@ void Engine::handleKeyInput(uint32_t key, bool pressed) 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 @@ -478,17 +569,23 @@ void Engine::Update(TimeDelta deltaTime) UpdateCameraControls(deltaTime); } - // Update all entities (guard against null unique_ptrs) - for (auto &entity : entities) + // 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; { - if (!entity) + std::shared_lock lk(entitiesMutex); + snapshot.reserve(entities.size()); + for (auto &uptr : entities) { - continue; + snapshot.push_back(uptr.get()); } - if (!entity->IsActive()) - { + } + for (Entity *entity : snapshot) + { + if (!entity || !entity->IsActive()) continue; - } entity->Update(deltaTime); } } @@ -507,8 +604,24 @@ void Engine::Render() 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(entities, activeCamera, imguiSystem.get()); + renderer->Render(snapshot, activeCamera, imguiSystem.get()); } std::chrono::milliseconds Engine::CalculateDeltaTimeMs() @@ -574,8 +687,14 @@ void Engine::UpdateCameraControls(TimeDelta deltaTime) if (imguiSystem && imguiSystem->IsCameraTrackingEnabled()) { // Find the first active ball entity - auto ballEntityIt = std::ranges::find_if(entities, [](auto const &entity) { return entity->IsActive() && (entity->GetName().find("Ball_") != std::string::npos); }); - Entity *ballEntity = ballEntityIt != entities.end() ? ballEntityIt->get() : nullptr; + 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) { diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h index 329940bd..6f58027b 100644 --- a/attachments/simple_engine/engine.h +++ b/attachments/simple_engine/engine.h @@ -18,7 +18,10 @@ #include #include +#include +#include #include +#include #include #include @@ -218,9 +221,23 @@ class Engine 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; + // Active camera CameraComponent *activeCamera = nullptr; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index a3f3387f..83c153aa 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -271,6 +271,11 @@ class Renderer */ 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); + /** * @brief Wait for the device to be idle. */ @@ -630,6 +635,16 @@ class Renderer 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{}; + } } /** @@ -717,6 +732,8 @@ class Renderer */ void RequestAccelerationStructureBuild() { + // 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 @@ -726,6 +743,7 @@ class Renderer lastASBuildRequestReason = reason; else lastASBuildRequestReason = "(no reason)"; + watchdogSuppressed.store(true, std::memory_order_relaxed); asBuildRequested.store(true, std::memory_order_release); } @@ -734,17 +752,17 @@ class Renderer * @param entities The entities to include in the acceleration structures. * @return True if successful, false otherwise. */ - bool buildAccelerationStructures(const std::vector> &entities); + bool buildAccelerationStructures(const std::vector &entities); // Refit/UPDATE the TLAS with latest entity transforms (no rebuild) - bool refitTopLevelAS(const std::vector> &entities); + bool refitTopLevelAS(const std::vector &entities); /** * @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); + bool updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vector &entities); /** * @brief Create or resize light storage buffers to accommodate the given number of lights. @@ -806,6 +824,10 @@ class Renderer */ 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); + /** * @brief Recreate the instance buffer for an entity that had its instances cleared. * @@ -1174,6 +1196,12 @@ class Renderer // 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::atomic pendingEntityPreallocQueued{false}; + void ProcessPendingEntityPreallocations(); + // Descriptor set layouts (declared before pools and sets) vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; vk::raii::DescriptorSetLayout pbrDescriptorSetLayout = nullptr; @@ -1181,13 +1209,15 @@ class Renderer vk::raii::PipelineLayout pbrTransparentPipelineLayout = nullptr; // The texture that will hold a snapshot of the opaque scene - vk::raii::Image opaqueSceneColorImage{nullptr}; - vk::raii::DeviceMemory opaqueSceneColorImageMemory{nullptr}; // <-- Standard Vulkan memory - vk::raii::ImageView opaqueSceneColorImageView{nullptr}; - vk::raii::Sampler opaqueSceneColorSampler{nullptr}; - - // A descriptor set for the opaque scene color texture. We will have one for each frame in flight - // to match the swapchain images. + // 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; @@ -1291,7 +1321,10 @@ class Renderer // Entities needing descriptor set refresh due to streamed textures std::mutex dirtyEntitiesMutex; - std::unordered_set descriptorDirtyEntities; + // 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; @@ -1351,11 +1384,12 @@ class Renderer std::unique_ptr instanceBufferAllocation = nullptr; void *instanceBufferMapped = nullptr; - // Tracks whether binding 0 (UBO) has been written at least once for each frame. - // This lets us avoid re-writing descriptor binding 0 every frame and prevents - // update-after-bind warnings while keeping initialization correct when a frame - // first becomes current. - std::vector uboBindingWritten; // size = MAX_FRAMES_IN_FLIGHT + // 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 @@ -1363,6 +1397,18 @@ class Renderer // 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 + + // 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; + 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{}; }; std::unordered_map entityResources; @@ -1469,8 +1515,11 @@ class Renderer vk::raii::Buffer tlasUpdateScratchBuffer{nullptr}; std::unique_ptr tlasUpdateScratchAllocation; - // Maximum number of frames in flight - const uint32_t MAX_FRAMES_IN_FLIGHT = 1u; + // 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 --- // CPU-side frustum culling toggle and last-frame stats @@ -1508,6 +1557,9 @@ class Renderer std::atomic lastFrameUpdateTime; 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 @@ -1598,14 +1650,18 @@ class Renderer 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> &entities); + void renderReflectionPass(vk::raii::CommandBuffer &cmd, + const glm::vec4 &planeWS, + CameraComponent *camera, + const std::vector &entities); // 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); + // ===================== Culling helpers ===================== struct FrustumPlanes { diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 6141a161..14e951cb 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -52,7 +52,8 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( // Watchdog thread function - monitors frame updates and aborts if application hangs static void WatchdogThreadFunc(std::atomic *lastFrameTime, - std::atomic *running) + std::atomic *running, + std::atomic *suppressed) { std::cout << "[Watchdog] Started - will abort if no frame updates for 5+ seconds\n"; @@ -65,13 +66,15 @@ static void WatchdogThreadFunc(std::atomicload(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 : 5; - if (elapsed >= 5) + if (elapsed >= allowedSeconds) { // APPLICATION HAS HUNG - no frame updates for 5+ seconds std::cerr << "\n\n"; @@ -341,7 +344,7 @@ bool Renderer::Initialize(const std::string &appName, bool enableValidationLayer // 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); + watchdogThread = std::thread(WatchdogThreadFunc, &lastFrameUpdateTime, &watchdogRunning, &watchdogSuppressed); initialized = true; return true; @@ -528,10 +531,11 @@ void Renderer::Cleanup() defaultTextureResources.textureImageAllocation = nullptr; // 7) Opaque scene color and related descriptors - opaqueSceneColorSampler = nullptr; - opaqueSceneColorImageView = nullptr; - opaqueSceneColorImageMemory = nullptr; - opaqueSceneColorImage = nullptr; + opaqueSceneColorSampler = nullptr; + opaqueSceneColorImages.clear(); + opaqueSceneColorImageAllocations.clear(); + opaqueSceneColorImageViews.clear(); + opaqueSceneColorImageLayouts.clear(); // 7.5) Ray query output image and acceleration structures rayQueryOutputImageView = nullptr; diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index 778b2429..dc7d7975 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -206,8 +206,15 @@ bool Renderer::createPBRDescriptorSetLayout() 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); } - transparentDescriptorSetLayout = vk::raii::DescriptorSetLayout(device, transparentLayoutInfo); return true; } diff --git a/attachments/simple_engine/renderer_ray_query.cpp b/attachments/simple_engine/renderer_ray_query.cpp index 939dc89b..571f6803 100644 --- a/attachments/simple_engine/renderer_ray_query.cpp +++ b/attachments/simple_engine/renderer_ray_query.cpp @@ -40,7 +40,7 @@ vk::DeviceAddress getBufferDeviceAddress(const vk::raii::Device &device, vk::Buf * @param entities The entities to include in the acceleration structures. * @return True if successful, false otherwise. */ -bool Renderer::buildAccelerationStructures(const std::vector> &entities) +bool Renderer::buildAccelerationStructures(const std::vector &entities) { if (!accelerationStructureEnabled || !rayQueryEnabled) { @@ -50,7 +50,19 @@ bool Renderer::buildAccelerationStructures(const std::vector std::chrono::milliseconds(200)) + { + lastFrameUpdateTime.store(now, std::memory_order_relaxed); + lastKick = now; + } + }; + kickWatchdog(); + + std::cout << "Building acceleration structures for " << entities.size() << " entities..." << std::endl; // PRECHECK: Determine how many renderable entities and unique meshes are READY right now. // If the counts would shrink compared to the last successful build (e.g., streaming not done), @@ -64,9 +76,9 @@ bool Renderer::buildAccelerationStructures(const std::vector meshToBLASProbe; - for (const auto &entityPtr : entities) + for (Entity *entity : entities) { - Entity *entity = entityPtr.get(); + kickWatchdog(); if (!entity || !entity->IsActive()) { skippedInactive++; @@ -156,9 +168,9 @@ bool Renderer::buildAccelerationStructures(const std::vectorIsActive()) { skippedInactive++; @@ -269,11 +281,7 @@ bool Renderer::buildAccelerationStructures(const std::vector 0 && i % 50 == 0) - { - lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); - } + kickWatchdog(); MeshComponent *meshComp = uniqueMeshes[i]; auto &meshRes = meshResources.at(meshComp); @@ -459,6 +467,7 @@ bool Renderer::buildAccelerationStructures(const std::vectorGetComponent(); uint32_t blasIndex = meshToBLAS.at(meshComp); @@ -472,6 +481,7 @@ bool Renderer::buildAccelerationStructures(const std::vector> &entities) +bool Renderer::refitTopLevelAS(const std::vector &entities) { try { @@ -1138,9 +1157,18 @@ bool Renderer::refitTopLevelAS(const std::vector> &entit std::lock_guard lock(queueMutex); graphicsQueue.submit(submitInfo, *fence); } - if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) + // Wait with periodic watchdog kicks to avoid false hang detection on long refits. + while (true) { - std::cerr << "Failed to wait for TLAS refit fence\n"; + vk::Result r = device.waitForFences({*fence}, VK_TRUE, /*timeout*/ 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; + } + std::cerr << "Failed to wait for TLAS refit fence: " << vk::to_string(r) << "\n"; return false; } return true; @@ -1160,7 +1188,7 @@ bool Renderer::refitTopLevelAS(const std::vector> &entit * @param frameIndex The frame index to update. * @return True if successful, false otherwise. */ -bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vector> &entities) +bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vector &entities) { if (!rayQueryEnabled || !accelerationStructureEnabled) { diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index c0e610d1..7ad79620 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -307,10 +307,10 @@ void Renderer::destroyReflectionResources() } } -void Renderer::renderReflectionPass(vk::raii::CommandBuffer &cmd, - const glm::vec4 &planeWS, - CameraComponent *camera, - const std::vector> &entities) +void Renderer::renderReflectionPass(vk::raii::CommandBuffer &cmd, + const glm::vec4 &planeWS, + CameraComponent *camera, + const std::vector &entities) { if (reflections.empty()) return; @@ -409,9 +409,8 @@ void Renderer::renderReflectionPass(vk::raii::CommandBuffer & } // Render all entities with meshes (skip transparency; glass revisit later) - for (const auto &uptr : entities) + for (Entity *entity : entities) { - Entity *entity = uptr.get(); if (!entity || !entity->IsActive()) continue; auto meshComponent = entity->GetComponent(); @@ -491,8 +490,10 @@ bool Renderer::createImageViews() { try { - opaqueSceneColorImage.clear(); - opaqueSceneColorImageView.clear(); + opaqueSceneColorImages.clear(); + opaqueSceneColorImageAllocations.clear(); + opaqueSceneColorImageViews.clear(); + opaqueSceneColorImageLayouts.clear(); opaqueSceneColorSampler.clear(); // Resize image views vector swapChainImageViews.clear(); @@ -772,6 +773,12 @@ void Renderer::recreateSwapChain() 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.clear(); + resources.basicUboBindingWritten.clear(); + resources.pbrImagesWritten.clear(); + resources.basicImagesWritten.clear(); } } @@ -949,14 +956,15 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity *entity // and we have a valid previous-frame reflection render target to sample from. ubo.reflectionPass = 0; bool reflReady = false; - if (enablePlanarReflections && !reflections.empty()) - { - // CRITICAL FIX: Use currentFrame (frame-in-flight index) instead of currentImage (swapchain index) - // Reflection resources are per-frame-in-flight, not per-swapchain-image - uint32_t prev = currentImage > 0 ? (currentImage - 1) : (static_cast(reflections.size()) - 1); - auto &rtPrev = reflections[prev]; - reflReady = !(rtPrev.colorView == nullptr) && !(rtPrev.colorSampler == nullptr); - } + if (enablePlanarReflections && !reflections.empty()) + { + // Use currentFrame (frame-in-flight index). Sample the previous frame's reflection RT + // so that the texture is fully written before it is read on this frame. + const uint32_t count = static_cast(reflections.size()); + const uint32_t prev = (currentFrame + count - 1u) % count; + auto &rtPrev = reflections[prev]; + reflReady = !(rtPrev.colorView == nullptr) && !(rtPrev.colorSampler == nullptr); + } ubo.reflectionEnabled = reflReady ? 1 : 0; ubo.reflectionVP = sampleReflectionVP; ubo.clipPlaneWS = currentReflectionPlane; @@ -982,8 +990,131 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity *entity std::memcpy(dst, &ubo, sizeof(ubo)); } -// Render the scene +void Renderer::ensureEntityMaterialCache(Entity *entity) +{ + if (!entity) + return; + + auto it = entityResources.find(entity); + if (it == entityResources.end()) + return; + auto &res = it->second; + 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 = false; + + 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 (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); + 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) { // Update watchdog timestamp to prove frame is progressing lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); @@ -1014,9 +1145,19 @@ void Renderer::Render(const std::vector> &entities, Came bool rayQueryRenderedThisFrame = false; // Wait for the previous frame's work on this frame slot to complete - if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) - { - std::cerr << "Warning: Failed to wait for fence on frame " << currentFrame << std::endl; + // 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). + while (true) + { + vk::Result r = device.waitForFences({*inFlightFences[currentFrame]}, VK_TRUE, /*timeout*/ 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; + } + std::cerr << "Warning: Failed to wait for fence on frame " << currentFrame << ": " << vk::to_string(r) << std::endl; return; } @@ -1027,6 +1168,10 @@ void Renderer::Render(const std::vector> &entities, Came // 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. 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). + ProcessPendingEntityPreallocations(); // Process deferred AS deletion queue at safe point (after fence wait) // Increment frame counters and delete AS structures that are no longer in use @@ -1053,15 +1198,24 @@ void Renderer::Render(const std::vector> &entities, Came // This makes the TLAS grow as streaming/allocations complete, then settle (no rebuild spam). if (rayQueryEnabled && accelerationStructureEnabled) { - size_t readyRenderableCount = 0; - size_t readyUniqueMeshCount = 0; - { - std::map meshToBLASProbe; - for (const auto &uptr : entities) + size_t readyRenderableCount = 0; + size_t readyUniqueMeshCount = 0; { - Entity *e = uptr.get(); - if (!e || !e->IsActive()) - continue; + 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()) { @@ -1149,20 +1303,30 @@ void Renderer::Render(const std::vector> &entities, Came // 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); + 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; { - std::map meshToBLASProbe; - for (const auto &uptr : entities) + // 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; { - Entity *e = uptr.get(); - if (!e || !e->IsActive()) - continue; + 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()) { @@ -1217,14 +1381,63 @@ void Renderer::Render(const std::vector> &entities, Came } else { - // CRITICAL: Wait for ALL GPU work to complete BEFORE building AS. - // External synchronization required (VVL): serialize against queue submits/present. - // This ensures no command buffers are still using vertex/index buffers that the AS build will reference. - WaitIdle(); + 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); + + // CRITICAL: Ensure previous GPU work is complete BEFORE building AS. + // We used to call `WaitIdle()` here, but that can block for multiple seconds on large scenes + // and falsely trip the watchdog (no `lastFrameUpdateTime` updates while blocked). + // + // Instead, wait for all *other* frame-in-flight fences to signal using a finite timeout loop + // and kick the watchdog while we wait. + // IMPORTANT: 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; + fencesToWait.reserve(inFlightFences.size() > 0 ? (inFlightFences.size() - 1) : 0); + for (uint32_t i = 0; i < static_cast(inFlightFences.size()); ++i) + { + if (i == currentFrame) + continue; + if (inFlightFences[i] != nullptr) + { + fencesToWait.push_back(*inFlightFences[i]); + } + } + if (!fencesToWait.empty()) + { + while (true) + { + vk::Result r = device.waitForFences(fencesToWait, VK_TRUE, /*timeout*/ 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; + } + std::cerr << "Warning: waitForFences failed before AS build: " << vk::to_string(r) << std::endl; + break; + } + } + } if (buildAccelerationStructures(entities)) { asBuildRequested.store(false, std::memory_order_release); + // AS build request resolved; restore normal watchdog sensitivity. + watchdogSuppressed.store(false, std::memory_order_relaxed); // Freeze only when the built TLAS is "full" (>=95% of static opaque renderables) if (asFreezeAfterFullBuild) { @@ -1276,9 +1489,8 @@ void Renderer::Render(const std::vector> &entities, Came // and initialize only binding 0 (UBO) for the current frame if not already done. { uint32_t entityProcessCount = 0; - for (const auto &uptr : entities) + for (Entity *entity : entities) { - Entity *entity = uptr.get(); if (!entity || !entity->IsActive()) continue; auto meshComponent = entity->GetComponent(); @@ -1319,6 +1531,15 @@ void Renderer::Render(const std::vector> &entities, Came /*imagesOnly=*/false, /*uboOnly=*/true); + // Basic/Phong pipeline also needs a per-frame UBO init at the safe point. + // Descriptor sets for non-current frames are allocated but may not be initialized yet. + updateDescriptorSetsForFrame(entity, + texPath, + /*usePBR=*/false, + currentFrame, + /*imagesOnly=*/false, + /*uboOnly=*/true); + // Cold-initialize image bindings for CURRENT frame once to avoid per-frame black flashes. // This writes PBR b1..b5 and Basic b1 with either real textures or shared defaults. // It does not touch UBO (handled above). @@ -2053,9 +2274,8 @@ void Renderer::Render(const std::vector> &entities, Came renderReflectionPass(commandBuffers[currentFrame], planeWS, camera, entities); } - for (const auto &uptr : entities) + for (Entity *entity : entities) { - Entity *entity = uptr.get(); if (!entity || !entity->IsActive()) continue; auto meshComponent = entity->GetComponent(); @@ -2077,41 +2297,11 @@ void Renderer::Render(const std::vector> &entities, Came } lastCullingVisibleCount++; bool useBlended = false; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + ensureEntityMaterialCache(entity); + auto entityIt = entityResources.find(entity); + if (entityIt != entityResources.end()) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) - { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - Material *material = modelLoader->GetMaterial(materialName); - // Classify as blended only for true alpha-blend materials, glass or liquids, or high transmission. - // This avoids shunting most opaque materials into the transparent pass (which skips the off-screen buffer). - bool isBlendedMat = false; - if (material) - { - bool alphaBlend = (material->alphaMode == "BLEND"); - bool highTransmission = (material->transmissionFactor > 0.2f); - bool glassLike = material->isGlass; - bool liquidLike = material->isLiquid; - isBlendedMat = alphaBlend || highTransmission || glassLike || liquidLike; - } - if (isBlendedMat) - { - useBlended = true; - } - } - } - } + useBlended = entityIt->second.cachedIsBlended; } // Ensure all entities are considered regardless of reflections setting. @@ -2181,32 +2371,13 @@ void Renderer::Render(const std::vector> &entities, Came // rendering liquid volumes before glass shells so bar glasses look // correctly filled. This is a heuristic based on material flags. auto classify = [this](Entity *e) { - bool hasGlass = false; - bool hasLiquid = false; - if (!e || !modelLoader) + if (!e) return std::pair{false, false}; - - const std::string &name = e->GetName(); - size_t tagPos = name.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < name.length()) - { - std::string remainder = name.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - if (Material *m = modelLoader->GetMaterial(materialName)) - { - hasGlass = m->isGlass; - hasLiquid = m->isLiquid; - } - } - } - } - return std::pair{hasGlass, hasLiquid}; + ensureEntityMaterialCache(e); + auto it = entityResources.find(e); + if (it == entityResources.end()) + return std::pair{false, false}; + return std::pair{it->second.cachedIsGlass, it->second.cachedIsLiquid}; }; auto [aIsGlass, aIsLiquid] = classify(a); @@ -2237,9 +2408,8 @@ void Renderer::Render(const std::vector> &entities, Came // Build list of non-blended entities std::vector opaqueEntities; opaqueEntities.reserve(entities.size()); - for (const auto &uptr : entities) + for (Entity *entity : entities) { - Entity *entity = uptr.get(); if (!entity || !entity->IsActive() || blendedSet.contains(entity)) continue; auto meshComponent = entity->GetComponent(); @@ -2292,27 +2462,10 @@ void Renderer::Render(const std::vector> &entities, Came // where fragments would be discarded by alpha test. These will write depth during // the opaque color pass using the standard opaque pipeline. bool isAlphaMasked = false; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + ensureEntityMaterialCache(entity); + if (entityIt != entityResources.end() && entityIt->second.materialCacheValid) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) - { - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - if (Material *m = modelLoader->GetMaterial(materialName)) - { - isAlphaMasked = (m->alphaMode == "MASK"); - } - } - } - } + isAlphaMasked = (entityIt->second.cachedMaterialProps.alphaMask > 0.5f); } // Fallback: infer mask from baseColor texture alpha usage hint if (!isAlphaMasked) @@ -2398,21 +2551,49 @@ void Renderer::Render(const std::vector> &entities, Came } // PASS 1: RENDER OPAQUE OBJECTS TO OFF-SCREEN TEXTURE - // Transition off-screen color from last frame's sampling to attachment write (Sync2) - vk::ImageMemoryBarrier2 oscToColor2{.srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader, - .srcAccessMask = vk::AccessFlagBits2::eShaderRead, + // 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, - .oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal, + .oldLayout = oscOldLayout, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = *opaqueSceneColorImage, + .image = *opaqueSceneColorImages[currentFrame], .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; - vk::DependencyInfo depOscToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &oscToColor2}; + vk::DependencyInfo depOscToColor{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &oscToColor2}; commandBuffers[currentFrame].pipelineBarrier2(depOscToColor); + if (currentFrame < opaqueSceneColorImageLayouts.size()) + { + opaqueSceneColorImageLayouts[currentFrame] = vk::ImageLayout::eColorAttachmentOptimal; + } // Clear the off-screen target at the start of opaque rendering to a neutral black background - vk::RenderingAttachmentInfo colorAttachment{.imageView = *opaqueSceneColorImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})}; + vk::RenderingAttachmentInfo colorAttachment{.imageView = *opaqueSceneColorImageViews[currentFrame], .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue(std::array{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}; @@ -2423,9 +2604,8 @@ void Renderer::Render(const std::vector> &entities, Came commandBuffers[currentFrame].setScissor(0, scissor); { uint32_t opaqueDrawsThisPass = 0; - for (const auto &uptr : entities) + for (Entity *entity : entities) { - Entity *entity = uptr.get(); if (!entity || !entity->IsActive() || (blendedSet.contains(entity))) continue; auto meshComponent = entity->GetComponent(); @@ -2444,27 +2624,11 @@ void Renderer::Render(const std::vector> &entities, Came // Determine if this entity uses alpha masking so we can bypass the post-prepass // read-only pipeline and use the normal depth-writing opaque pipeline instead. bool isAlphaMaskedOpaque = false; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + ensureEntityMaterialCache(entity); + auto entityItForMask = entityResources.find(entity); + if (entityItForMask != entityResources.end() && entityItForMask->second.materialCacheValid) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) - { - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - if (Material *m = modelLoader->GetMaterial(materialName)) - { - isAlphaMaskedOpaque = (m->alphaMode == "MASK"); - } - } - } - } + isAlphaMaskedOpaque = (entityItForMask->second.cachedMaterialProps.alphaMask > 0.5f); } // Fallback based on texture hint if material flag not set if (!isAlphaMaskedOpaque) @@ -2563,64 +2727,10 @@ void Renderer::Render(const std::vector> &entities, Came pushConstants.emissiveStrength = 1.0f; pushConstants.hasEmissiveStrengthExtension = false; } - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) + ensureEntityMaterialCache(entity); + if (entityIt != entityResources.end() && entityIt->second.materialCacheValid && entityIt->second.cachedMaterial) { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) - { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - Material *material = modelLoader->GetMaterial(materialName); - if (material) - { - // Base factors - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - - // Texture set flags (-1 = no texture) - pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; - // physical descriptor: MR or SpecGloss - if (material->useSpecularGlossiness) - { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; - pushConstants.glossinessFactor = material->glossinessFactor; - pushConstants.specularFactor = material->specularFactor; - } - else - { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; - } - pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; - pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; - pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; - - // Emissive and transmission/IOR - pushConstants.emissiveFactor = material->emissive; - pushConstants.emissiveStrength = material->emissiveStrength; - // Heuristic: consider emissive strength extension present when strength != 1.0 - pushConstants.hasEmissiveStrengthExtension = (std::abs(material->emissiveStrength - 1.0f) > 1e-6f); - pushConstants.transmissionFactor = material->transmissionFactor; - pushConstants.ior = material->ior; - - // Alpha mask handling - pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; - } - } - } - } + pushConstants = entityIt->second.cachedMaterialProps; } // If no explicit MASK from a material, infer it from the baseColor texture's alpha usage if (pushConstants.alphaMask < 0.5f) @@ -2698,10 +2808,14 @@ void Renderer::Render(const std::vector> &entities, Came .newLayout = vk::ImageLayout::eShaderReadOnlyOptimal, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = *opaqueSceneColorImage, + .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::eTopOfPipe, @@ -2793,33 +2907,12 @@ void Renderer::Render(const std::vector> &entities, Came if (!meshComponent || entityIt == entityResources.end() || meshIt == meshResources.end()) continue; - // Resolve material for this entity (if any) - Material *material = nullptr; - if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) - { - std::string entityName = entity->GetName(); - size_t tagPos = entityName.find("_Material_"); - if (tagPos != std::string::npos) - { - size_t afterTag = tagPos + std::string("_Material_").size(); - if (afterTag < entityName.length()) - { - // Entity name format: "modelName_Material__" - // Find the next underscore after the material index to get the actual material name - std::string remainder = entityName.substr(afterTag); - size_t nextUnderscore = remainder.find('_'); - if (nextUnderscore != std::string::npos && nextUnderscore + 1 < remainder.length()) - { - std::string materialName = remainder.substr(nextUnderscore + 1); - material = modelLoader->GetMaterial(materialName); - } - } - } - } + ensureEntityMaterialCache(entity); + Material *material = entityIt->second.cachedMaterial; // Choose pipeline: specialized glass pipeline for architectural glass, // otherwise the generic blended PBR pipeline. - bool useGlassPipeline = material && material->isGlass; + bool useGlassPipeline = entityIt->second.cachedIsGlass; vk::raii::Pipeline *desiredPipeline = useGlassPipeline ? &glassGraphicsPipeline : &pbrBlendGraphicsPipeline; if (desiredPipeline != activeTransparentPipeline) { @@ -2868,45 +2961,10 @@ void Renderer::Render(const std::vector> &entities, Came // pushConstants.ior already 1.5f default if (material) { - // Base factors - pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); - pushConstants.metallicFactor = material->metallic; - pushConstants.roughnessFactor = material->roughness; - - // Texture set flags (-1 = no texture) - pushConstants.baseColorTextureSet = material->albedoTexturePath.empty() ? -1 : 0; - if (material->useSpecularGlossiness) - { - pushConstants.useSpecGlossWorkflow = 1; - pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; - pushConstants.glossinessFactor = material->glossinessFactor; - pushConstants.specularFactor = material->specularFactor; - } - else - { - pushConstants.useSpecGlossWorkflow = 0; - pushConstants.physicalDescriptorTextureSet = material->metallicRoughnessTexturePath.empty() ? -1 : 0; - } - pushConstants.normalTextureSet = material->normalTexturePath.empty() ? -1 : 0; - pushConstants.occlusionTextureSet = material->occlusionTexturePath.empty() ? -1 : 0; - pushConstants.emissiveTextureSet = material->emissiveTexturePath.empty() ? -1 : 0; - - // Emissive and transmission/IOR - pushConstants.emissiveFactor = material->emissive; - pushConstants.emissiveStrength = material->emissiveStrength; - pushConstants.hasEmissiveStrengthExtension = false; // Material has emissive strength data - pushConstants.transmissionFactor = material->transmissionFactor; - pushConstants.ior = material->ior; - - // Alpha mask handling - pushConstants.alphaMask = (material->alphaMode == "MASK") ? 1.0f : 0.0f; - pushConstants.alphaMaskCutoff = material->alphaCutoff; - + pushConstants = entityIt->second.cachedMaterialProps; // For bar liquids and similar volumes, we want the fill to be - // clearly visible rather than fully transmissive. For these - // materials, disable the transmission branch in the PBR shader - // and treat them as regular alpha-blended PBR surfaces. - if (material->isLiquid) + // clearly visible rather than fully transmissive. + if (entityIt->second.cachedIsLiquid) { pushConstants.transmissionFactor = 0.0f; } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index edd7dd7a..a45c0270 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1591,7 +1591,8 @@ bool Renderer::preAllocateEntityResources(Entity *entity) auto it = entityResources.find(entity); if (it != entityResources.end()) { - it->second.uboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + 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); } @@ -1755,6 +1756,65 @@ void Renderer::EnqueueMeshUploads(const std::vector &meshes) } } +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::ProcessPendingEntityPreallocations() +{ + if (!pendingEntityPreallocQueued.load(std::memory_order_relaxed)) + return; + + std::vector toProcess; + { + std::lock_guard lk(pendingEntityPreallocMutex); + if (pendingEntityPrealloc.empty()) + { + pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); + return; + } + toProcess.swap(pendingEntityPrealloc); + pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); + } + + // De-dup to avoid repeated work if loader enqueues overlapping batches + std::sort(toProcess.begin(), toProcess.end()); + toProcess.erase(std::unique(toProcess.begin(), toProcess.end()), toProcess.end()); + + std::vector batch; + batch.reserve(toProcess.size()); + for (Entity *e : toProcess) + { + if (!e || !e->IsActive()) + continue; + // Only preallocate for entities that actually have renderable mesh data + if (!e->GetComponent()) + continue; + batch.push_back(e); + } + if (batch.empty()) + return; + + // Execute GPU resource creation on the render thread at the safe point. + // Keep failures non-fatal so loading can continue; we'll retry on subsequent frames. + if (!preAllocateEntityResourcesBatch(batch)) + { + std::cerr << "Warning: batch entity GPU preallocation failed; will retry" << std::endl; + } +} + // Execute pending mesh uploads on the render thread after the per-frame fence wait void Renderer::ProcessPendingMeshUploads() { @@ -2007,12 +2067,12 @@ void Renderer::createTransparentDescriptorSets() transparentDescriptorSets = vk::raii::DescriptorSets(device, allocInfo); } - // Update each descriptor set to point to our single off-screen opaque color image + // 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 = *opaqueSceneColorImageView, + .imageView = *opaqueSceneColorImageViews[i], .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; vk::WriteDescriptorSet descriptorWrite{ @@ -2069,22 +2129,33 @@ bool Renderer::createOpaqueSceneColorResources() { try { - // Create the image - 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, // <-- Note the new usage flags - vk::MemoryPropertyFlagBits::eDeviceLocal); + 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); - opaqueSceneColorImage = std::move(image); - // We don't need a member for the allocation, it's managed by the unique_ptr + 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); - // Create the image view - opaqueSceneColorImageView = createImageView(opaqueSceneColorImage, swapChainImageFormat, vk::ImageAspectFlagBits::eColor); + 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 the sampler + // Create (or recreate) the sampler (shared across frames) vk::SamplerCreateInfo samplerInfo{ .magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear, @@ -2383,12 +2454,12 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima { 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; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue}; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; submitInfo.pNext = &timelineInfo; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &*uploadsTimeline; @@ -2452,12 +2523,12 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi { 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; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue}; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; submitInfo.pNext = &timelineInfo; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &*uploadsTimeline; @@ -3266,20 +3337,22 @@ void Renderer::StartUploadsWorker(size_t workerCount) uint64_t signalValue = 0; bool canSignal = uploadsTimeline != nullptr; { - std::lock_guard lock(queueMutex); - vk::SubmitInfo submit{}; - if (canSignal) - { - signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; - vk::TimelineSemaphoreSubmitInfo timelineInfo{.signalSemaphoreValueCount = 1, .pSignalSemaphoreValues = &signalValue}; - submit.pNext = &timelineInfo; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &*uploadsTimeline; - } - submit.commandBufferCount = 1; - submit.pCommandBuffers = &*cb; - transferQueue.submit(submit, *fence); - } + 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); + } device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); // Perf accounting for the batch @@ -3436,8 +3509,12 @@ 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); - descriptorDirtyEntities.insert(entity); + auto &mask = descriptorDirtyEntities[entity]; + mask |= allFramesMask; } bool Renderer::updateDescriptorSetsForFrame(Entity *entity, @@ -3493,10 +3570,14 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, vk::DescriptorBufferInfo bufferInfo{.buffer = *entityIt->second.uniformBuffers[frameIndex], .range = sizeof(UniformBufferObject)}; - // Ensure uboBindingWritten vector is sized - if (entityIt->second.uboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) + // Ensure per-pipeline UBO init tracking is sized + if (entityIt->second.pbrUboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) { - entityIt->second.uboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + entityIt->second.pbrUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + if (entityIt->second.basicUboBindingWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + entityIt->second.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); } if (usePBR) @@ -3510,14 +3591,14 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, if (uboOnly) { // Avoid re-writing if we already initialized this frame's UBO binding - if (!entityIt->second.uboBindingWritten[frameIndex]) + if (!entityIt->second.pbrUboBindingWritten[frameIndex]) { writes.push_back({.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}); { std::lock_guard lk(descriptorMutex); device.updateDescriptorSets(writes, {}); } - entityIt->second.uboBindingWritten[frameIndex] = true; + entityIt->second.pbrUboBindingWritten[frameIndex] = true; } return true; } @@ -3560,7 +3641,7 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, // CRITICAL FIX: Only mark UBO as written if we actually wrote it (not during imagesOnly updates) if (!imagesOnly) { - entityIt->second.uboBindingWritten[frameIndex] = true; + entityIt->second.pbrUboBindingWritten[frameIndex] = true; } } else @@ -3583,13 +3664,16 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, // If uboOnly is requested for basic pipeline, only write binding 0 if (uboOnly) { - std::array descriptorWrites = { - vk::WriteDescriptorSet{.dstSet = *targetDescriptorSets[frameIndex], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo}}; + if (!entityIt->second.basicUboBindingWritten[frameIndex]) { - std::lock_guard lk(descriptorMutex); - device.updateDescriptorSets(descriptorWrites, {}); + 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, {}); + } + entityIt->second.basicUboBindingWritten[frameIndex] = true; } - entityIt->second.uboBindingWritten[frameIndex] = true; return true; } std::array descriptorWrites = { @@ -3599,7 +3683,7 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, std::lock_guard lk(descriptorMutex); device.updateDescriptorSets(descriptorWrites, {}); } - entityIt->second.uboBindingWritten[frameIndex] = true; + entityIt->second.basicUboBindingWritten[frameIndex] = true; } } return true; @@ -3607,18 +3691,26 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, void Renderer::ProcessDirtyDescriptorsForFrame(uint32_t frameIndex) { - std::vector dirty; + if (frameIndex >= 32u) + return; + const uint32_t frameBit = (1u << frameIndex); + + std::vector toProcess; { std::lock_guard lk(dirtyEntitiesMutex); if (descriptorDirtyEntities.empty()) return; - dirty.reserve(descriptorDirtyEntities.size()); - for (auto *e : descriptorDirtyEntities) - dirty.push_back(e); - descriptorDirtyEntities.clear(); + toProcess.reserve(descriptorDirtyEntities.size()); + for (auto &[e, mask] : descriptorDirtyEntities) + { + if (e && (mask & frameBit)) + { + toProcess.push_back(e); + } + } } - for (Entity *entity : dirty) + for (Entity *entity : toProcess) { if (!entity) continue; @@ -3636,6 +3728,22 @@ void Renderer::ProcessDirtyDescriptorsForFrame(uint32_t frameIndex) updateDescriptorSetsForFrame(entity, 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, @@ -3825,16 +3933,16 @@ void Renderer::uploadImageFromStaging(vk::Buffer st uint64_t signalValue = 0; { std::lock_guard lock(queueMutex); - vk::SubmitInfo submit{}; + vk::SubmitInfo submit{}; + vk::TimelineSemaphoreSubmitInfo timelineInfo{}; // keep alive through submit if (canSignalTimeline) { signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue}; - submit.pNext = &timelineInfo; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &*uploadsTimeline; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &signalValue; + submit.pNext = &timelineInfo; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &*uploadsTimeline; } submit.commandBufferCount = 1; submit.pCommandBuffers = &*cb; @@ -3933,20 +4041,20 @@ void Renderer::generateMipmaps(vk::Image image, bool canSignalTimeline = uploadsTimeline != nullptr; uint64_t signalValue = 0; { - std::lock_guard lock(queueMutex); - vk::SubmitInfo submit{}; - if (canSignalTimeline) - { - signalValue = uploadTimelineLastSubmitted.fetch_add(1, std::memory_order_relaxed) + 1; - vk::TimelineSemaphoreSubmitInfo timelineInfo{ - .signalSemaphoreValueCount = 1, - .pSignalSemaphoreValues = &signalValue}; - submit.pNext = &timelineInfo; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &*uploadsTimeline; - } - submit.commandBufferCount = 1; - submit.pCommandBuffers = &*cb; + 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) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 9c28dc8a..ca58bf06 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -399,11 +399,10 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, // Pre-allocate Vulkan resources for all geometry entities in a single batched pass if (!geometryEntities.empty()) { - if (!renderer->preAllocateEntityResourcesBatch(geometryEntities)) - { - std::cerr << "Failed to pre-allocate resources for one or more geometry entities in batch" << std::endl; - // For now, continue; individual entities may still be partially usable - } + // Scene loading runs on a background thread. Do NOT perform Vulkan allocations + // or mutate renderer resource maps here. Enqueue the batch so the render thread can + // perform the GPU work safely at its frame-start safe point. + renderer->EnqueueEntityPreallocationBatch(geometryEntities); } // Set up animations if the model has any From f5335e8b324a55ec86e3cbdb7b3044f579366f68 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Wed, 17 Dec 2025 15:09:57 -0800 Subject: [PATCH 06/24] Add instance buffer recreation handling in Renderer This gets windows to run out of the box. --- attachments/simple_engine/renderer.h | 8 +-- .../simple_engine/renderer_resources.cpp | 52 +++++++++++++------ attachments/simple_engine/scene_loading.cpp | 8 +-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 83c153aa..1c849ad8 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -827,6 +827,7 @@ class Renderer // 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. @@ -1197,10 +1198,11 @@ class Renderer void ProcessPendingMeshUploads(); // --- Pending entity GPU preallocation (enqueued by scene loader thread; executed on render thread) --- - std::mutex pendingEntityPreallocMutex; + std::mutex pendingEntityPreallocMutex; std::vector pendingEntityPrealloc; - std::atomic pendingEntityPreallocQueued{false}; - void ProcessPendingEntityPreallocations(); + std::vector pendingInstanceBufferRecreations; + std::atomic pendingEntityPreallocQueued{false}; + void ProcessPendingEntityPreallocations(); // Descriptor set layouts (declared before pools and sets) vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index a45c0270..546f03f9 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1772,46 +1772,68 @@ void Renderer::EnqueueEntityPreallocationBatch(const std::vector &enti 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; - std::vector toProcess; + std::vector toPreallocate; + std::vector toRecreateInstances; { std::lock_guard lk(pendingEntityPreallocMutex); - if (pendingEntityPrealloc.empty()) + if (pendingEntityPrealloc.empty() && pendingInstanceBufferRecreations.empty()) { pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); return; } - toProcess.swap(pendingEntityPrealloc); + toPreallocate.swap(pendingEntityPrealloc); + toRecreateInstances.swap(pendingInstanceBufferRecreations); pendingEntityPreallocQueued.store(false, std::memory_order_relaxed); } - // De-dup to avoid repeated work if loader enqueues overlapping batches - std::sort(toProcess.begin(), toProcess.end()); - toProcess.erase(std::unique(toProcess.begin(), toProcess.end()), toProcess.end()); + // De-dup preallocations + std::sort(toPreallocate.begin(), toPreallocate.end()); + toPreallocate.erase(std::unique(toPreallocate.begin(), toPreallocate.end()), toPreallocate.end()); std::vector batch; - batch.reserve(toProcess.size()); - for (Entity *e : toProcess) + batch.reserve(toPreallocate.size()); + for (Entity *e : toPreallocate) { if (!e || !e->IsActive()) continue; - // Only preallocate for entities that actually have renderable mesh data if (!e->GetComponent()) continue; batch.push_back(e); } - if (batch.empty()) - return; - // Execute GPU resource creation on the render thread at the safe point. - // Keep failures non-fatal so loading can continue; we'll retry on subsequent frames. - if (!preAllocateEntityResourcesBatch(batch)) + if (!batch.empty()) + { + if (!preAllocateEntityResourcesBatch(batch)) + { + std::cerr << "Warning: batch entity GPU preallocation failed; will retry" << std::endl; + } + } + + // Process instance buffer recreations + for (Entity *e : toRecreateInstances) { - std::cerr << "Warning: batch entity GPU preallocation failed; will retry" << std::endl; + if (!e || !e->IsActive()) + continue; + if (!recreateInstanceBuffer(e)) + { + std::cerr << "Warning: failed to recreate instance buffer for entity " << e->GetName() << std::endl; + } } } diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index ca58bf06..76c82ccb 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -496,9 +496,9 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, // Recreate the GPU instance buffer with a single identity instance // The old buffer still had multiple instances, so we need to update it - if (renderer && !renderer->recreateInstanceBuffer(nodeEntity)) + if (renderer) { - std::cerr << "[Animation] Failed to recreate instance buffer for reused entity" << std::endl; + renderer->EnqueueInstanceBufferRecreation(nodeEntity); } } } @@ -546,9 +546,9 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, } // Pre-allocate resources for this new entity - if (renderer && !renderer->preAllocateEntityResources(nodeEntity)) + if (renderer) { - std::cerr << "[Animation] Failed to pre-allocate resources for " << entityName << std::endl; + renderer->EnqueueEntityPreallocationBatch({nodeEntity}); } std::cout << "[Animation] Created new entity '" << entityName << "' for node " << nodeIndex << std::endl; From 41365a0895786a912051434ff1031972651dc808 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Wed, 17 Dec 2025 15:39:42 -0800 Subject: [PATCH 07/24] Add checks for AS/ray query support in acceleration structure build paths --- attachments/simple_engine/renderer.h | 4 ++++ attachments/simple_engine/renderer_rendering.cpp | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 1c849ad8..0ea58b89 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -732,6 +732,8 @@ class Renderer */ void RequestAccelerationStructureBuild() { + if (!accelerationStructureEnabled || !rayQueryEnabled) + return; // 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); @@ -739,6 +741,8 @@ class Renderer // Overload with reason tracking for diagnostics void RequestAccelerationStructureBuild(const char *reason) { + if (!accelerationStructureEnabled || !rayQueryEnabled) + return; if (reason) lastASBuildRequestReason = reason; else diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 7ad79620..75fbba36 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -1473,7 +1473,16 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } else { - std::cout << "Failed to build acceleration structures, will retry next frame" << std::endl; + if (!accelerationStructureEnabled || !rayQueryEnabled) + { + // Permanent failure due to lack of support; do not retry. + asBuildRequested.store(false, std::memory_order_release); + watchdogSuppressed.store(false, std::memory_order_relaxed); + } + else + { + std::cout << "Failed to build acceleration structures, will retry next frame" << std::endl; + } } // Reset dev override after one use asDevOverrideAllowRebuild = false; From 1ee46ba190f90724f8a95a0dd28addd419957528 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 00:44:35 -0800 Subject: [PATCH 08/24] Improve Watchdog functionality and mesh resource handling - Added progress tracking to Watchdog for better diagnostics during long operations. - Enhanced Vulkan extension support checks with reusable helper. - Optimized fencing and timeline semaphore handling for resource uploads. - Improved staging buffer usage and batch processing for entity resources. The windows debug now loads in under a minute and displays ray query as default and at 80fps. Forward+ causes raster pass to average around 30fps while normal PBR raster, phong and no culling are all around 50 fps. --- attachments/simple_engine/crash_reporter.h | 160 +++++- attachments/simple_engine/imgui_system.cpp | 80 ++- attachments/simple_engine/main.cpp | 9 + attachments/simple_engine/model_loader.cpp | 62 +-- attachments/simple_engine/renderer.h | 186 ++++++- attachments/simple_engine/renderer_core.cpp | 52 +- .../simple_engine/renderer_ray_query.cpp | 300 ++++++++--- .../simple_engine/renderer_rendering.cpp | 351 ++++++++----- .../simple_engine/renderer_resources.cpp | 479 ++++++++++++++---- attachments/simple_engine/renderer_utils.cpp | 64 +++ attachments/simple_engine/scene_loading.cpp | 75 ++- 11 files changed, 1401 insertions(+), 417 deletions(-) diff --git a/attachments/simple_engine/crash_reporter.h b/attachments/simple_engine/crash_reporter.h index 2a974f8d..34abef83 100644 --- a/attachments/simple_engine/crash_reporter.h +++ b/attachments/simple_engine/crash_reporter.h @@ -16,6 +16,7 @@ */ #pragma once +#include #include #include #include @@ -25,10 +26,11 @@ #include #include #include +#include #ifdef _WIN32 -# include # include +# include # pragma comment(lib, "dbghelp.lib") #elif defined(__APPLE__) || defined(__linux__) # include @@ -118,18 +120,7 @@ class CrashReporter */ void HandleCrash(const std::string &message) { - std::lock_guard lock(mutex); - - LOG_FATAL("CrashReporter", "Crash detected: " + message); - - // Generate minidump - GenerateMinidump(message); - - // Call registered callbacks - for (const auto &callback : crashCallbacks) - { - callback(message); - } + HandleCrashInternal(message, nullptr); } /** @@ -161,7 +152,7 @@ class CrashReporter * @brief Generate a minidump. * @param message The crash message. */ - void GenerateMinidump(const std::string &message) + void GenerateMinidump(const std::string &message, void *platformExceptionPointers = nullptr) { // Get current time for filename auto now = std::chrono::system_clock::now(); @@ -171,12 +162,37 @@ class CrashReporter // Create minidump filename std::string filename = minidumpDir + "/" + appName + "_" + timeStr + ".dmp"; + std::string report = minidumpDir + "/" + appName + "_" + timeStr + ".txt"; - LOG_INFO("CrashReporter", "Generating minidump: " + filename); + // Also write a small sidecar text file so users can quickly see the exception code/address + // without needing a debugger. + try + { + std::ofstream rep(report, std::ios::out | std::ios::trunc); + rep << "Crash Report for " << appName << " " << appVersion << "\n"; + rep << "Timestamp: " << timeStr << "\n"; + rep << "Message: " << message << "\n"; +#ifdef _WIN32 + if (platformExceptionPointers) + { + auto *exPtrs = reinterpret_cast(platformExceptionPointers); + if (exPtrs && exPtrs->ExceptionRecord) + { + rep << "ExceptionCode: 0x" << std::hex << exPtrs->ExceptionRecord->ExceptionCode << std::dec << "\n"; + rep << "ExceptionAddress: " << exPtrs->ExceptionRecord->ExceptionAddress << "\n"; + rep << "ExceptionFlags: 0x" << std::hex << exPtrs->ExceptionRecord->ExceptionFlags << std::dec << "\n"; + } + } +#endif + } + catch (...) + { + } // Generate minidump based on platform #ifdef _WIN32 // Windows implementation + EXCEPTION_POINTERS *exPtrs = reinterpret_cast(platformExceptionPointers); HANDLE hFile = CreateFileA( filename.c_str(), GENERIC_WRITE, @@ -188,19 +204,19 @@ class CrashReporter if (hFile != INVALID_HANDLE_VALUE) { - MINIDUMP_EXCEPTION_INFORMATION exInfo; + MINIDUMP_EXCEPTION_INFORMATION exInfo{}; exInfo.ThreadId = GetCurrentThreadId(); - exInfo.ExceptionPointers = NULL; // Would be set in a real exception handler + exInfo.ExceptionPointers = exPtrs; exInfo.ClientPointers = FALSE; - MiniDumpWriteDump( - GetCurrentProcess(), - GetCurrentProcessId(), - hFile, - MiniDumpNormal, - &exInfo, - NULL, - NULL); + MINIDUMP_EXCEPTION_INFORMATION *exInfoPtr = exPtrs ? &exInfo : nullptr; + MiniDumpWriteDump(GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + MiniDumpNormal, + exInfoPtr, + NULL, + NULL); CloseHandle(hFile); } @@ -232,7 +248,10 @@ class CrashReporter } #endif - LOG_INFO("CrashReporter", "Minidump generated: " + filename); + // Best-effort stderr note (stdout/stderr redirection will capture this even if DebugSystem isn't initialized) + std::fprintf(stderr, "[CrashReporter] Wrote minidump: %s\n", filename.c_str()); + std::fprintf(stderr, "[CrashReporter] Wrote report: %s\n", report.c_str()); + std::fflush(stderr); } private: @@ -259,6 +278,80 @@ class CrashReporter // Crash callbacks std::unordered_map> crashCallbacks; int nextCallbackId = 0; + std::atomic handlingCrash{false}; + +#ifdef _WIN32 + static bool ShouldCaptureException(EXCEPTION_POINTERS *exInfo, bool unhandled) + { + if (unhandled) + return true; + if (!exInfo || !exInfo->ExceptionRecord) + return false; + const DWORD code = exInfo->ExceptionRecord->ExceptionCode; + const DWORD flags = exInfo->ExceptionRecord->ExceptionFlags; + // Ignore common first-chance C++ exceptions and breakpoint exceptions. + if (code == 0xE06D7363u /* MSVC C++ EH */ || code == 0x80000003u /* breakpoint */) + return false; + // Capture likely-fatal errors and non-continuable exceptions. + if ((flags & EXCEPTION_NONCONTINUABLE) != 0) + return true; + switch (code) + { + case 0xC0000409u: // STATUS_STACK_BUFFER_OVERRUN + case 0xC0000005u: // STATUS_ACCESS_VIOLATION + case 0xC000001Du: // STATUS_ILLEGAL_INSTRUCTION + case 0xC00000FDu: // STATUS_STACK_OVERFLOW + case 0xC0000374u: // STATUS_HEAP_CORRUPTION + return true; + default: + return false; + } + } +#endif + +#ifdef _WIN32 + void *vectoredHandlerHandle = nullptr; +#endif + + void HandleCrashInternal(const std::string &message, void *platformExceptionPointers) + { + bool expected = false; + if (!handlingCrash.compare_exchange_strong(expected, true)) + { + // Already handling a crash; avoid recursion. + return; + } + std::lock_guard lock(mutex); + + std::string msg = message; + (void) platformExceptionPointers; + +#ifdef _WIN32 + if (platformExceptionPointers) + { + auto *exPtrs = reinterpret_cast(platformExceptionPointers); + if (exPtrs && exPtrs->ExceptionRecord) + { + const DWORD code = exPtrs->ExceptionRecord->ExceptionCode; + void *addr = exPtrs->ExceptionRecord->ExceptionAddress; + char buf[128]; + std::snprintf(buf, sizeof(buf), " (code=0x%08lX, addr=%p)", static_cast(code), addr); + msg += buf; + } + } +#endif + + LOG_FATAL("CrashReporter", "Crash detected: " + msg); + + // Generate minidump + GenerateMinidump(msg, platformExceptionPointers); + + // Call registered callbacks + for (const auto &callback : crashCallbacks) + { + callback.second(msg); + } + } /** * @brief Install platform-specific crash handlers. @@ -267,8 +360,16 @@ class CrashReporter { #ifdef _WIN32 // Windows implementation + // Vectored handler runs before SEH/unhandled filters and is more likely to fire for fast-fail style crashes. + vectoredHandlerHandle = AddVectoredExceptionHandler(1, [](EXCEPTION_POINTERS *exInfo) -> LONG { + if (CrashReporter::ShouldCaptureException(exInfo, /*unhandled=*/false)) + { + CrashReporter::GetInstance().HandleCrashInternal("Vectored exception", exInfo); + } + return EXCEPTION_CONTINUE_SEARCH; + }); SetUnhandledExceptionFilter([](EXCEPTION_POINTERS *exInfo) -> LONG { - CrashReporter::GetInstance().HandleCrash("Unhandled exception"); + CrashReporter::GetInstance().HandleCrashInternal("Unhandled exception", exInfo); return EXCEPTION_EXECUTE_HANDLER; }); #else @@ -303,6 +404,11 @@ class CrashReporter #ifdef _WIN32 // Windows implementation SetUnhandledExceptionFilter(NULL); + if (vectoredHandlerHandle) + { + RemoveVectoredExceptionHandler(vectoredHandlerHandle); + vectoredHandlerHandle = nullptr; + } #else // Unix implementation signal(SIGSEGV, SIG_DFL); diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp index 6fc4012d..6804df1a 100644 --- a/attachments/simple_engine/imgui_system.cpp +++ b/attachments/simple_engine/imgui_system.cpp @@ -165,13 +165,11 @@ void ImGuiSystem::NewFrame() ImGui::NewFrame(); - // Loading overlay: show only a fullscreen progress bar while the model - // itself is loading. Once the scene is ready and geometry is visible, - // we no longer block the view with a full-screen progress bar. + // 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 uint32_t scheduled = renderer->GetTextureTasksScheduled(); - const uint32_t completed = renderer->GetTextureTasksCompleted(); const bool modelLoading = renderer->IsLoading(); if (modelLoading) { @@ -202,11 +200,40 @@ void ImGuiSystem::NewFrame() const float barY = dispSize.y * 0.45f; ImGui::SetCursorPos(ImVec2(barX, barY)); ImGui::BeginGroup(); - float frac = (scheduled > 0) ? (float) completed / (float) scheduled : 0.0f; + + // 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 scene..."); + 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(); } @@ -226,6 +253,41 @@ void ImGuiSystem::NewFrame() 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) { @@ -234,7 +296,9 @@ void ImGuiSystem::NewFrame() const float windowWidth = std::min(260.0f, dispSize.x * 0.35f); const float windowHeight = 120.0f; - const ImVec2 winPos(dispSize.x - windowWidth - 10.0f, 10.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)); diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp index f289677c..38b8233c 100644 --- a/attachments/simple_engine/main.cpp +++ b/attachments/simple_engine/main.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ #include "camera_component.h" +#include "crash_reporter.h" #include "engine.h" #include "scene_loading.h" #include "transform_component.h" @@ -63,6 +64,7 @@ void SetupScene(Engine *engine) if (auto *renderer = engine->GetRenderer()) { renderer->SetLoading(true); + renderer->SetLoadingPhase(Renderer::LoadingPhase::Textures); } std::thread([engine] { LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); @@ -107,6 +109,10 @@ 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; @@ -122,11 +128,14 @@ int main(int, char *[]) // 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; } } diff --git a/attachments/simple_engine/model_loader.cpp b/attachments/simple_engine/model_loader.cpp index 8ce54e55..9e920144 100644 --- a/attachments/simple_engine/model_loader.cpp +++ b/attachments/simple_engine/model_loader.cpp @@ -560,13 +560,11 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) // Load texture data (embedded or external) const auto &image = gltfModel.images[imageIndex]; - std::cout << " Image data size: " << image.image.size() << ", URI: " << image.uri << std::endl; if (!image.image.empty()) { // Always use memory-based upload (KTX2 already decoded by SetImageLoader) renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); material->albedoTexturePath = textureId; - std::cout << " Scheduled base color texture upload from memory: " << textureId << std::endl; } else if (!image.uri.empty()) { @@ -575,7 +573,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath, true); material->albedoTexturePath = textureId; - std::cout << " Scheduled base color KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -602,7 +599,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { // Load embedded texture data asynchronously renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded metallic-roughness texture upload: " << textureId << std::endl; } else if (!image.uri.empty()) { @@ -611,7 +607,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath); material->metallicRoughnessTexturePath = textureId; - std::cout << " Scheduled metallic-roughness KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -659,8 +654,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); material->normalTexturePath = textureId; - std::cout << " Scheduled normal texture upload from memory: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { @@ -669,7 +662,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath); material->normalTexturePath = textureId; - std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -696,8 +688,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { // Schedule embedded texture upload renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded occlusion texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { @@ -706,7 +696,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath); material->occlusionTexturePath = textureId; - std::cout << " Scheduled occlusion KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -733,8 +722,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { // Schedule embedded texture upload renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded emissive texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { @@ -743,7 +730,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath); material->emissiveTexturePath = textureId; - std::cout << " Scheduled emissive KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -810,7 +796,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) // Schedule async load; libktx decoding will occur on renderer worker threads renderer->LoadTextureAsync(texIdOrPath, true); mat->albedoTexturePath = texIdOrPath; - std::cout << " Scheduled base color KTX2 file load (KHR_specGloss): " << texIdOrPath << std::endl; } if (mat->albedoTexturePath.empty() && !image.image.empty()) { @@ -818,7 +803,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) texIdOrPath = "gltf_baseColor_" + std::to_string(texIndex); renderer->LoadTextureFromMemoryAsync(texIdOrPath, image.image.data(), image.width, image.height, image.component, true); mat->albedoTexturePath = texIdOrPath; - std::cout << " Scheduled base color texture upload from memory (KHR_specGloss): " << texIdOrPath << std::endl; } } } @@ -866,14 +850,13 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) // Ensure the file exists before attempting to load if (std::filesystem::exists(cand)) { - // Schedule async load; libktx decoding will occur on renderer worker threads - renderer->LoadTextureAsync(cand, true); - mat->albedoTexturePath = cand; - std::cout << " Scheduled derived base color KTX2 load from normal sibling: " << cand << std::endl; - break; - } - } - } + // Schedule async load; libktx decoding will occur on renderer worker threads + renderer->LoadTextureAsync(cand, true); + mat->albedoTexturePath = cand; + break; + } + } + } } // Secondary heuristic: scan glTF images for base color by material-name match when still missing @@ -918,7 +901,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); mat->albedoTexturePath = textureId; - std::cout << " Scheduled base color upload from memory (by name): " << textureId << std::endl; break; } else @@ -926,7 +908,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) // Fallback: offload KTX2 file load to renderer threads renderer->LoadTextureAsync(textureId); mat->albedoTexturePath = textureId; - std::cout << " Scheduled base color KTX2 load from file (by name): " << textureId << std::endl; break; } } @@ -1614,12 +1595,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component, true); loadedTextures.insert(textureId); - std::cout << " Scheduled baseColor texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } - else - { - std::cout << " Using cached baseColor texture: " << textureId << std::endl; } } else @@ -1661,7 +1636,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); materialMesh.baseColorTexturePath = textureId; materialMesh.texturePath = textureId; - std::cout << " Scheduled baseColor upload from memory (heuristic): " << textureId << std::endl; } else { @@ -1669,7 +1643,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->LoadTextureAsync(textureId, true); materialMesh.baseColorTexturePath = textureId; materialMesh.texturePath = textureId; - std::cout << " Scheduled baseColor KTX2 load from file (heuristic): " << textureId << std::endl; } break; } @@ -1695,8 +1668,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { // Load embedded texture data renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded normal texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { @@ -1705,7 +1676,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) renderer->RegisterTextureAlias(textureId, filePath); renderer->LoadTextureAsync(filePath); materialMesh.normalTexturePath = textureId; - std::cout << " Scheduled normal KTX2 load from file: " << filePath << " (alias for " << textureId << ")" << std::endl; } else { @@ -1732,7 +1702,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); materialMesh.normalTexturePath = textureId; - std::cout << " Scheduled normal upload from memory (heuristic): " << textureId << std::endl; } else { @@ -1758,13 +1727,11 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) // Load texture data (embedded or external) const auto &image = gltfModel.images[texture.source]; - if (!image.image.empty()) - { - renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - materialMesh.metallicRoughnessTexturePath = textureId; - std::cout << " Scheduled metallic-roughness texture upload: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; - } + if (!image.image.empty()) + { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + materialMesh.metallicRoughnessTexturePath = textureId; + } else { std::cerr << " Warning: No decoded bytes for metallic-roughness texture index " << texIndex << std::endl; @@ -1850,7 +1817,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); materialMesh.occlusionTexturePath = textureId; - std::cout << " Scheduled occlusion upload from memory (heuristic): " << textureId << std::endl; } else { @@ -1880,15 +1846,12 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { // Load embedded texture data renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); - std::cout << " Scheduled embedded emissive texture: " << textureId - << " (" << image.width << "x" << image.height << ")" << std::endl; } else if (!image.uri.empty()) { // Record external texture file path (loaded later by renderer) std::string texturePath = baseTexturePath + image.uri; materialMesh.emissiveTexturePath = texturePath; - std::cout << " External emissive texture path: " << texturePath << std::endl; } } } @@ -1909,7 +1872,6 @@ bool ModelLoader::ParseGLTF(const std::string &filename, Model *model) { std::string texturePath = baseTexturePath + imageUri; materialMesh.emissiveTexturePath = texturePath; - std::cout << " Found external emissive texture for " << materialName << ": " << texturePath << std::endl; break; } } diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index 0ea58b89..ca191deb 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -281,6 +282,18 @@ class Renderer */ 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. @@ -468,6 +481,47 @@ class Renderer 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. @@ -495,22 +549,80 @@ class Renderer // 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. - bool IsLoading() const + enum class LoadingPhase : uint32_t { - return (loadingFlag.load() || criticalJobsOutstanding.load() > 0) && !initialLoadComplete.load(); + Scene = 0, + Textures, + Physics, + AccelerationStructures, + Finalizing + }; + LoadingPhase GetLoadingPhase() const + { + return static_cast(loadingPhase.load(std::memory_order_relaxed)); } - void SetLoading(bool v) + const char *GetLoadingPhaseName() const { - loadingFlag.store(v); - if (!v) + switch (GetLoadingPhase()) { - // Mark initial load complete; non-critical streaming can continue in background. - initialLoadComplete.store(true); + 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"; } - else + } + 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); + initialLoadComplete.store(false, std::memory_order_relaxed); + SetLoadingPhase(LoadingPhase::Scene); } } @@ -734,6 +846,18 @@ class Renderer { 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); @@ -743,6 +867,14 @@ class Renderer { 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; else @@ -1055,6 +1187,11 @@ class Renderer 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; @@ -1196,6 +1333,16 @@ class Renderer 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) @@ -1311,6 +1458,9 @@ class Renderer 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}; @@ -1356,6 +1506,15 @@ class Renderer 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; @@ -1485,12 +1644,17 @@ class Renderer 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 @@ -1561,6 +1725,10 @@ class Renderer // === 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 diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp index 14e951cb..506bacb3 100644 --- a/attachments/simple_engine/renderer_core.cpp +++ b/attachments/simple_engine/renderer_core.cpp @@ -53,10 +53,10 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallbackVkRaii( // 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 *suppressed, + std::atomic *progressLabel, + std::atomic *progressIndex) { - std::cout << "[Watchdog] Started - will abort if no frame updates for 5+ seconds\n"; - while (running->load(std::memory_order_relaxed)) { std::this_thread::sleep_for(std::chrono::seconds(5)); @@ -68,20 +68,39 @@ static void WatchdogThreadFunc(std::atomicload(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 : 5; + const int64_t allowedSeconds = (suppressed && suppressed->load(std::memory_order_relaxed)) ? 60 : 10; if (elapsed >= allowedSeconds) { - // APPLICATION HAS HUNG - no frame updates for 5+ seconds + // 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"; @@ -344,7 +363,9 @@ bool Renderer::Initialize(const std::string &appName, bool enableValidationLayer // 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); + 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; @@ -989,8 +1010,15 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) 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 = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) != deviceExtensions.end(); + auto hasRobust2 = hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); vk::PhysicalDeviceRobustness2FeaturesEXT robust2Enable{}; if (hasRobust2) { @@ -1003,7 +1031,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) } // Prepare Dynamic Rendering Local Read features if extension is enabled and supported - auto hasLocalRead = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME) != deviceExtensions.end(); + auto hasLocalRead = hasExtension(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME); vk::PhysicalDeviceDynamicRenderingLocalReadFeaturesKHR localReadEnable{}; if (hasLocalRead && localReadSupported.dynamicRenderingLocalRead) { @@ -1011,7 +1039,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) } // Prepare Shader Tile Image features if extension is enabled and supported - auto hasTileImage = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME) != deviceExtensions.end(); + auto hasTileImage = hasExtension(VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME); vk::PhysicalDeviceShaderTileImageFeaturesEXT tileImageEnable{}; if (hasTileImage) { @@ -1024,7 +1052,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) } // Prepare Acceleration Structure features if extension is enabled and supported - auto hasAccelerationStructure = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) != deviceExtensions.end(); + auto hasAccelerationStructure = hasExtension(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME); vk::PhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureEnable{}; if (hasAccelerationStructure && accelerationStructureSupported.accelerationStructure) { @@ -1032,7 +1060,7 @@ bool Renderer::createLogicalDevice(bool enableValidationLayers) } // Prepare Ray Query features if extension is enabled and supported - auto hasRayQuery = std::find(deviceExtensions.begin(), deviceExtensions.end(), VK_KHR_RAY_QUERY_EXTENSION_NAME) != deviceExtensions.end(); + auto hasRayQuery = hasExtension(VK_KHR_RAY_QUERY_EXTENSION_NAME); vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryEnable{}; if (hasRayQuery && rayQuerySupported.rayQuery) { diff --git a/attachments/simple_engine/renderer_ray_query.cpp b/attachments/simple_engine/renderer_ray_query.cpp index 571f6803..a9706e0c 100644 --- a/attachments/simple_engine/renderer_ray_query.cpp +++ b/attachments/simple_engine/renderer_ray_query.cpp @@ -50,6 +50,52 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities try { + const auto asStartCpu = std::chrono::steady_clock::now(); + + // --- UI progress instrumentation (for long AS builds) --- + // We update these frequently during BLAS/TLAS builds so the loading overlay + // can display meaningful progress if the build takes > ~10 seconds. + auto nowNs = []() -> uint64_t { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + }; + auto setASUi = [&](bool active, const char *stage, float progress, uint32_t done, uint32_t total) { + asBuildUiActive.store(active, std::memory_order_relaxed); + asBuildUiStage.store(stage ? stage : "", std::memory_order_relaxed); + asBuildUiProgress.store(std::clamp(progress, 0.0f, 1.0f), std::memory_order_relaxed); + asBuildUiDone.store(done, std::memory_order_relaxed); + asBuildUiTotal.store(total, std::memory_order_relaxed); + // Also drive the main loading overlay progress while we're in the AS phase. + if (GetLoadingPhase() == LoadingPhase::AccelerationStructures) + { + SetLoadingPhaseProgress(progress); + } + }; + // Start timer if not already running + if (asBuildUiStartNs.load(std::memory_order_relaxed) == 0) + { + asBuildUiStartNs.store(nowNs(), std::memory_order_relaxed); + } + setASUi(true, "AS: prepare", 0.0f, 0u, 0u); + struct ASBuildUiGuard + { + Renderer *r; + explicit ASBuildUiGuard(Renderer *rr) : r(rr) {} + ~ASBuildUiGuard() + { + if (!r) + return; + r->asBuildUiActive.store(false, std::memory_order_relaxed); + r->asBuildUiStage.store("idle", std::memory_order_relaxed); + r->asBuildUiProgress.store(0.0f, std::memory_order_relaxed); + r->asBuildUiDone.store(0u, std::memory_order_relaxed); + r->asBuildUiTotal.store(0u, std::memory_order_relaxed); + r->asBuildUiStartNs.store(0u, std::memory_order_relaxed); + } + } asUiGuard(this); + // Large scenes can take seconds to build BLAS/TLAS. Keep the watchdog alive while we work. auto lastKick = std::chrono::steady_clock::now(); auto kickWatchdog = [&]() { @@ -234,12 +280,15 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities if (uniqueMeshes.empty()) { - return true; + // Nothing ready yet (e.g., mesh uploads still pending). Treat as a transient + // condition so the caller can retry next frame without clearing the request. + setASUi(true, "AS: waiting on meshes", 0.0f, 0u, 0u); + return false; } // One concise build summary (no per-entity spam) std::cout << "Building AS: uniqueMeshes=" << uniqueMeshes.size() - << ", instances=" << renderableEntities.size() + << ", entities=" << renderableEntities.size() << " (skipped inactive=" << skippedInactive << ", noMesh=" << skippedNoMesh << ", noRes=" << skippedNoRes @@ -274,6 +323,10 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities // Build BLAS for each unique mesh blasStructures.resize(uniqueMeshes.size()); + // Progress model: BLAS phase dominates. Treat TLAS + post buffers as a few extra steps. + const uint32_t totalSteps = static_cast(uniqueMeshes.size()) + 3u; + setASUi(true, "AS: build BLAS", 0.0f, 0u, totalSteps); + // Keep scratch buffers alive until GPU execution completes (after fence wait) // Destroying them early causes "VkBuffer was destroy" validation errors and crashes std::vector scratchBuffers; @@ -282,6 +335,12 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities for (size_t i = 0; i < uniqueMeshes.size(); ++i) { kickWatchdog(); + // Update UI progress (BLAS) + setASUi(true, + "AS: build BLAS", + totalSteps > 0 ? static_cast(static_cast(i)) / static_cast(totalSteps) : 0.0f, + static_cast(i), + totalSteps); MeshComponent *meshComp = uniqueMeshes[i]; auto &meshRes = meshResources.at(meshComp); @@ -387,6 +446,12 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities // (Per-BLAS logging removed; keep logs quiet in production.) } + // BLAS done + setASUi(true, + "AS: build TLAS", + totalSteps > 0 ? static_cast(static_cast(uniqueMeshes.size())) / static_cast(totalSteps) : 0.0f, + static_cast(uniqueMeshes.size()), + totalSteps); // Barrier between BLAS and TLAS builds @@ -685,8 +750,8 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities scratchAllocations.push_back(std::move(tlasScratchAlloc)); // Ensure/update a persistent scratch buffer for TLAS UPDATE (refit) - // Allocate once sized to updateScratchSize - if (!*tlasUpdateScratchBuffer || !tlasUpdateScratchAllocation) + // Allocate once sized to updateScratchSize; recreate if needed for larger scenes + if (!*tlasUpdateScratchBuffer || !tlasUpdateScratchAllocation || tlasUpdateScratchAllocation->size < tlasSizeInfo.updateScratchSize) { auto [updBuf, updAlloc] = createBufferPooled( tlasSizeInfo.updateScratchSize, @@ -732,25 +797,21 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities } // Wait with periodic watchdog kicks to avoid false hang detection on large scenes. - while (true) - { - vk::Result r = device.waitForFences({*fence}, VK_TRUE, /*timeout*/ 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; - } - std::cerr << "Failed to wait for AS build fence: " << vk::to_string(r) << "\n"; - return false; - } + (void) waitForFencesSafe(*fence, VK_TRUE); + // TLAS build completed on GPU + setASUi(true, + "AS: upload buffers", + totalSteps > 0 ? static_cast(static_cast(uniqueMeshes.size()) + 1u) / static_cast(totalSteps) : 0.0f, + static_cast(uniqueMeshes.size()) + 1u, + totalSteps); // (Verbose TLAS composition dumps removed; keep logs quiet.) - // Record the counts we just built so we don't rebuild with smaller subsets later - lastASBuiltBLASCount = blasStructures.size(); - lastASBuiltInstanceCount = instanceCount; + // Record the counts we just built so we don't rebuild with smaller subsets later. + // Keep entity counts and TLAS instance counts separate to avoid unit mismatches. + lastASBuiltBLASCount = blasStructures.size(); + lastASBuiltInstanceCount = renderableEntities.size(); + lastASBuiltTlasInstanceCount = instanceCount; // Build geometry info buffer PER INSTANCE (same order as TLAS instances) // geometryInfos already populated above in TLAS instance loop @@ -776,6 +837,12 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities // (Verbose geometry info buffer stats removed.) } + // Post buffers done + setASUi(true, + "AS: finalize", + totalSteps > 0 ? static_cast(static_cast(uniqueMeshes.size()) + 2u) / static_cast(totalSteps) : 1.0f, + static_cast(uniqueMeshes.size()) + 2u, + totalSteps); // Build material buffer with real materials from ModelLoader { @@ -783,6 +850,7 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities // Collect unique materials with their indices from entities std::map materialIndexToName; + static constexpr uint32_t kMaxSupportedMaterialIndex = 100000u; size_t entityCount = 0; for (Entity *entity : renderableEntities) @@ -800,7 +868,13 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities { try { - uint32_t matIndex = std::stoi(entityName.substr(numStart, numEnd - numStart)); + uint32_t matIndex = std::stoi(entityName.substr(numStart, numEnd - numStart)); + if (matIndex > kMaxSupportedMaterialIndex) + { + // Malformed entity name (or unexpected content) could yield a huge index. + // Skip to avoid allocating an enormous material table or writing out of bounds. + continue; + } // Extract material name (everything after materialIndex_) std::string materialName = entityName.substr(numEnd + 1); @@ -865,6 +939,7 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities { maxMaterialIndex = std::max(maxMaterialIndex, index); } + maxMaterialIndex = std::min(maxMaterialIndex, kMaxSupportedMaterialIndex); // Ensure minimum size of 100 materials for safety (matches original implementation) uint32_t materialCount = std::max(maxMaterialIndex + 1, 100u); @@ -884,6 +959,8 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities size_t matProcessed = 0; for (const auto &[index, materialName] : materialIndexToName) { + if (index >= materials.size()) + continue; Material *sourceMat = modelLoader->GetMaterial(materialName); if (sourceMat) { @@ -1037,11 +1114,35 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities materialCountCPU = materials.size(); } + // The TLAS/material/geometry buffers and texture table contents may have changed. + // Mark ray query descriptor sets dirty so the render thread refreshes them at the next safe point. + const uint32_t allFramesMask = (MAX_FRAMES_IN_FLIGHT >= 32u) ? 0xFFFFFFFFu : ((1u << MAX_FRAMES_IN_FLIGHT) - 1u); + rayQueryDescriptorsDirtyMask.fetch_or(allFramesMask, std::memory_order_relaxed); + + setASUi(true, "AS: done", 1.0f, totalSteps, totalSteps); + const auto elapsedMs = std::chrono::duration_cast(std::chrono::steady_clock::now() - asStartCpu).count(); + std::cout << "AS build completed in " << (static_cast(elapsedMs) / 1000.0) + << "s (uniqueMeshes=" << uniqueMeshes.size() + << ", entities=" << renderableEntities.size() + << ", tlasInstances=" << instanceCount << ")\n"; return true; } catch (const std::exception &e) { - std::cerr << "Failed to build acceleration structures: " << e.what() << std::endl; + const uint64_t startNs = asBuildUiStartNs.load(std::memory_order_relaxed); + if (startNs != 0) + { + const uint64_t nowNs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + const double secs = (nowNs > startNs) ? (static_cast(nowNs - startNs) / 1'000'000'000.0) : 0.0; + std::cerr << "Failed to build acceleration structures after " << secs << "s: " << e.what() << std::endl; + } + else + { + std::cerr << "Failed to build acceleration structures: " << e.what() << std::endl; + } return false; } } @@ -1062,8 +1163,19 @@ bool Renderer::refitTopLevelAS(const std::vector &entities) if (!instPtr) return false; + 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; + } + }; + for (uint32_t i = 0; i < tlasInstanceCount; ++i) { + kickWatchdog(); const TlasInstanceRef &ref = tlasInstanceOrder[i]; Entity *entity = ref.entity; if (!entity || !entity->IsActive()) @@ -1158,19 +1270,7 @@ bool Renderer::refitTopLevelAS(const std::vector &entities) graphicsQueue.submit(submitInfo, *fence); } // Wait with periodic watchdog kicks to avoid false hang detection on long refits. - while (true) - { - vk::Result r = device.waitForFences({*fence}, VK_TRUE, /*timeout*/ 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; - } - std::cerr << "Failed to wait for TLAS refit fence: " << vk::to_string(r) << "\n"; - return false; - } + (void) waitForFencesSafe(*fence, VK_TRUE); return true; } catch (const std::exception &e) @@ -1194,6 +1294,10 @@ bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vect { return false; } + if (frameIndex >= MAX_FRAMES_IN_FLIGHT) + { + return false; + } // Do not update descriptors while descriptor sets are known invalid if (!descriptorSetsValid.load(std::memory_order_relaxed)) @@ -1273,6 +1377,21 @@ bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vect return false; } + // Avoid doing expensive updates every frame. + // Binding 6 is a large descriptor array; updating it each frame can stall the CPU badly. + if (rayQueryDescriptorsWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + rayQueryDescriptorsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } + const uint32_t bitMask = (1u << frameIndex); + const bool dirty = (rayQueryDescriptorsDirtyMask.load(std::memory_order_relaxed) & bitMask) != 0u; + const bool first = !rayQueryDescriptorsWritten[frameIndex]; + if (!dirty && !first) + { + // Nothing changed that requires descriptor rebind for this frame. + return true; + } + // Frame index alignment check: ensure we are updating descriptor set for the frame being recorded if (frameIndex != currentFrame) { @@ -1403,7 +1522,7 @@ bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vect // Binding 6: Ray Query texture table (combined image samplers) // IMPORTANT: Do NOT cache VkImageView/VkSampler handles across frames; textures can stream - // and their handles may be destroyed/recreated. Instead, rebuild image infos each update. + // and their handles may be destroyed/recreated. if (rayQueryTexKeys.size() < RQ_SLOT_DEFAULT_EMISSIVE + 1 || rayQueryTexFallbackSlots.size() < RQ_SLOT_DEFAULT_EMISSIVE + 1) { // Should be seeded during AS build; if not, fall back to using the generic default texture in all slots. @@ -1412,64 +1531,81 @@ bool Renderer::updateRayQueryDescriptorSets(uint32_t frameIndex, const std::vect rayQueryTexCount = std::max(rayQueryTexCount, static_cast(rayQueryTexKeys.size())); } - std::vector rqArray(RQ_MAX_TEX, vk::DescriptorImageInfo{ - .sampler = *defaultTextureResources.textureSampler, - .imageView = *defaultTextureResources.textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}); - - const uint32_t copyCount = std::min(rayQueryTexCount, RQ_MAX_TEX); - std::shared_lock texLock(textureResourcesMutex); + const uint32_t copyCount = std::min(rayQueryTexCount, RQ_MAX_TEX); + // First-time init writes the full array with defaults so the set is fully defined. + // Subsequent refreshes update only the active range [0, copyCount), which is much faster. + const bool initFullArray = first; + const uint32_t writeCount = initFullArray ? RQ_MAX_TEX : copyCount; + std::vector rqArray(writeCount, vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}); + if (copyCount > 0) + { + // Fill active slots under a short-lived shared lock, then release before taking descriptorMutex. + std::shared_lock texLock(textureResourcesMutex); + auto fillSlot = [&](uint32_t slot) { + if (slot >= copyCount) + return; + const std::string &key = rayQueryTexKeys[slot]; + if (!key.empty()) + { + auto itTex = textureResources.find(key); + if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + { + rqArray[slot].sampler = *itTex->second.textureSampler; + rqArray[slot].imageView = *itTex->second.textureImageView; + rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + return; + } + } - // Helper to fill a slot with a key (if ready) or fall back to its declared fallback slot. - auto fillSlot = [&](uint32_t slot) { - if (slot >= copyCount) - return; - const std::string &key = rayQueryTexKeys[slot]; - if (!key.empty()) - { - auto itTex = textureResources.find(key); - if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + // Not ready/missing: use slot-specific fallback. + uint32_t fb = (slot < rayQueryTexFallbackSlots.size()) ? rayQueryTexFallbackSlots[slot] : RQ_SLOT_DEFAULT_BASECOLOR; + if (fb >= copyCount) + fb = RQ_SLOT_DEFAULT_BASECOLOR; + const std::string &fbKey = (fb < rayQueryTexKeys.size()) ? rayQueryTexKeys[fb] : std::string{}; + if (!fbKey.empty()) { - rqArray[slot].sampler = *itTex->second.textureSampler; - rqArray[slot].imageView = *itTex->second.textureImageView; - rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; - return; + auto itTex = textureResources.find(fbKey); + if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + { + rqArray[slot].sampler = *itTex->second.textureSampler; + rqArray[slot].imageView = *itTex->second.textureImageView; + rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + } } - } + }; - // Not ready/missing: use slot-specific fallback. - uint32_t fb = (slot < rayQueryTexFallbackSlots.size()) ? rayQueryTexFallbackSlots[slot] : RQ_SLOT_DEFAULT_BASECOLOR; - if (fb >= copyCount) - fb = RQ_SLOT_DEFAULT_BASECOLOR; - const std::string &fbKey = (fb < rayQueryTexKeys.size()) ? rayQueryTexKeys[fb] : std::string{}; - if (!fbKey.empty()) + for (uint32_t i = 0; i < copyCount; ++i) { - auto itTex = textureResources.find(fbKey); - if (itTex != textureResources.end() && itTex->second.textureImageView != nullptr && itTex->second.textureSampler != nullptr) + // Kick watchdog occasionally during large descriptor table fills. + if ((i % 128u) == 0u) { - rqArray[slot].sampler = *itTex->second.textureSampler; - rqArray[slot].imageView = *itTex->second.textureImageView; - rqArray[slot].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); } + fillSlot(i); } - }; + } - // Fill all active slots. - for (uint32_t i = 0; i < copyCount; ++i) + if (writeCount > 0) { - fillSlot(i); + vk::WriteDescriptorSet texArrayWrite{}; + texArrayWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; + texArrayWrite.dstBinding = 6; + texArrayWrite.dstArrayElement = 0; + texArrayWrite.descriptorCount = writeCount; + texArrayWrite.descriptorType = vk::DescriptorType::eCombinedImageSampler; + texArrayWrite.pImageInfo = rqArray.data(); + writes.push_back(texArrayWrite); } - vk::WriteDescriptorSet texArrayWrite{}; - texArrayWrite.dstSet = *rayQueryDescriptorSets[frameIndex]; - texArrayWrite.dstBinding = 6; - texArrayWrite.dstArrayElement = 0; - texArrayWrite.descriptorCount = RQ_MAX_TEX; - texArrayWrite.descriptorType = vk::DescriptorType::eCombinedImageSampler; - texArrayWrite.pImageInfo = rqArray.data(); - writes.push_back(texArrayWrite); - - device.updateDescriptorSets(writes, nullptr); + { + std::lock_guard lk(descriptorMutex); + device.updateDescriptorSets(writes, nullptr); + } + rayQueryDescriptorsWritten[frameIndex] = true; + rayQueryDescriptorsDirtyMask.fetch_and(~bitMask, std::memory_order_relaxed); // No per-frame or one-shot debug prints here; keep logs quiet in production. diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 75fbba36..90bc7014 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -279,7 +279,7 @@ bool Renderer::createReflectionResources(uint32_t width, uint32_t height) std::lock_guard lock(queueMutex); graphicsQueue.submit(submit, *fence); } - (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); } return true; @@ -722,9 +722,7 @@ void Renderer::recreateSwapChain() } if (!allFences.empty()) { - if (device.waitForFences(allFences, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) - { - } + (void) waitForFencesSafe(allFences, VK_TRUE); } // Wait for the device to be idle before recreating the swap chain @@ -759,9 +757,7 @@ void Renderer::recreateSwapChain() // Wait for all command buffers to complete before clearing resources for (const auto &fence : inFlightFences) { - if (device.waitForFences(*fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) - { - } + (void) waitForFencesSafe(*fence, VK_TRUE); } // Clear all entity descriptor sets since they're now invalid (allocated from the old pool) @@ -785,6 +781,8 @@ void Renderer::recreateSwapChain() // 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 = nullptr; @@ -1118,6 +1116,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca { // 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); static bool firstRenderLogged = false; if (!firstRenderLogged) @@ -1147,31 +1146,24 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // 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). - while (true) - { - vk::Result r = device.waitForFences({*inFlightFences[currentFrame]}, VK_TRUE, /*timeout*/ 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; - } - std::cerr << "Warning: Failed to wait for fence on frame " << currentFrame << ": " << vk::to_string(r) << std::endl; - return; - } + watchdogProgressLabel.store("Render: wait inFlightFence", std::memory_order_relaxed); + (void) waitForFencesSafe(*inFlightFences[currentFrame], VK_TRUE); // 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 @@ -1193,11 +1185,15 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } } } + 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). - if (rayQueryEnabled && accelerationStructureEnabled) + // NOTE: This scan can be relatively heavy and is not needed for the default startup path. + // Only run it when opportunistic rebuilds are enabled. + if (rayQueryEnabled && accelerationStructureEnabled && asOpportunisticRebuildEnabled) { + watchdogProgressLabel.store("Render: AS readiness scan", std::memory_order_relaxed); size_t readyRenderableCount = 0; size_t readyUniqueMeshCount = 0; { @@ -1254,7 +1250,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } readyUniqueMeshCount = meshToBLASProbe.size(); } - if (asOpportunisticRebuildEnabled && !asFrozen && (readyRenderableCount > lastASBuiltInstanceCount || readyUniqueMeshCount > lastASBuiltBLASCount) && !asBuildRequested.load(std::memory_order_relaxed)) + if (!asFrozen && (readyRenderableCount > lastASBuiltInstanceCount || readyUniqueMeshCount > lastASBuiltBLASCount) && !asBuildRequested.load(std::memory_order_relaxed)) { std::cout << "AS rebuild requested: counts increased (built instances=" << lastASBuiltInstanceCount << ", ready instances=" << readyRenderableCount @@ -1277,33 +1273,48 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } } - // If in Ray Query static-only mode and TLAS not yet built post-load, request a one-time build now - if (currentRenderMode == RenderMode::RayQuery && IsRayQueryStaticOnly() && !IsLoading() && !*tlasStructure.handle && !asBuildRequested.load(std::memory_order_relaxed)) - { - RequestAccelerationStructureBuild("static-only initial 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)) { - // Defer TLAS/BLAS build while the scene is loading to avoid partial builds (e.g., only animated fans) - if (IsLoading()) + watchdogProgressLabel.store("Render: AS build request handling", std::memory_order_relaxed); + // Low-noise one-time diagnostic to confirm the render thread observes the request. + // (Helps debug cases where the app prints the request message but AS logic never runs.) + static bool loggedASRequestObserved = false; + if (!loggedASRequestObserved) { - // Keep the request flag set; we'll build once loading completes - static uint32_t asDeferredLoadingCounter = 0; - if ((asDeferredLoadingCounter++ % 120u) == 0u) - { - std::cout << "AS build deferred: scene still loading" << std::endl; - } + const uint64_t reqNs = asBuildRequestStartNs.load(std::memory_order_relaxed); + std::cout << "AS build request observed on render thread (loading=" << (IsLoading() ? "true" : "false") + << ", reqNs=" << reqNs << ")" << std::endl; + loggedASRequestObserved = true; + } + + // 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) { // 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); + loggedASRequestObserved = false; } else { @@ -1311,6 +1322,10 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca 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 = [&]() { @@ -1340,24 +1355,36 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca if (!meshComp) continue; totalRenderableEntities++; - try - { - auto it = meshResources.find(meshComp); - if (it == meshResources.end()) - continue; - const auto &res = it->second; - // STRICT readiness here too: uploads finished - if (res.vertexBufferSizeBytes != 0 || res.indexBufferSizeBytes != 0) - continue; - if (!*res.vertexBuffer || !*res.indexBuffer) - continue; - if (res.indexCount == 0) + 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; - } - catch (...) - { - continue; - } + } readyRenderableCount++; if (meshToBLASProbe.find(meshComp) == meshToBLASProbe.end()) { @@ -1367,20 +1394,32 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca readyUniqueMeshCount = meshToBLASProbe.size(); } const double readiness = (totalRenderableEntities > 0) ? static_cast(readyRenderableCount) / static_cast(totalRenderableEntities) : 0.0; - const double buildThreshold = 0.95; // build only when ~full scene is ready - if (readiness < buildThreshold && !asDevOverrideAllowRebuild) - { - static uint32_t asDeferredReadinessCounter = 0; - if ((asDeferredReadinessCounter++ % 120u) == 0u) + 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 = static_cast( + std::chrono::duration_cast( + 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) { - std::cout << "AS build deferred: readiness " << readyRenderableCount << "/" << totalRenderableEntities - << " entities (" << static_cast(readiness * 100.0) << "%), uniqueMeshesReady=" - << readyUniqueMeshCount << std::endl; + // 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 } - // 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; @@ -1417,71 +1456,74 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } if (!fencesToWait.empty()) { - while (true) - { - vk::Result r = device.waitForFences(fencesToWait, VK_TRUE, /*timeout*/ 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; - } - std::cerr << "Warning: waitForFences failed before AS build: " << vk::to_string(r) << std::endl; - break; - } + (void) waitForFencesSafe(fencesToWait, VK_TRUE); } } - if (buildAccelerationStructures(entities)) - { - asBuildRequested.store(false, std::memory_order_release); - // AS build request resolved; restore normal watchdog sensitivity. - watchdogSuppressed.store(false, std::memory_order_relaxed); - // Freeze only when the built TLAS is "full" (>=95% of static opaque renderables) - if (asFreezeAfterFullBuild) - { - const double threshold = 0.95; - if (totalRenderableEntities > 0 && static_cast(lastASBuiltInstanceCount) >= threshold * static_cast(totalRenderableEntities)) - { - asFrozen = true; - std::cout << "AS frozen after full build (instances=" << lastASBuiltInstanceCount - << "/" << totalRenderableEntities << ")" << std::endl; - } - else - { - std::cout << "AS not frozen yet (built instances=" << lastASBuiltInstanceCount - << ", total renderables=" << totalRenderableEntities << ")" << std::endl; - } - } - // One-line TLAS summary with device address - if (*tlasStructure.handle) - { - if (IsRayQueryStaticOnly()) - { - std::cout << "TLAS ready (static-only): instances=" << lastASBuiltInstanceCount - << ", BLAS=" << lastASBuiltBLASCount - << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << std::endl; - } - else - { - std::cout << "TLAS ready: instances=" << lastASBuiltInstanceCount - << ", BLAS=" << lastASBuiltBLASCount - << ", addr=0x" << std::hex << tlasStructure.deviceAddress << std::dec << 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); + } + loggedASRequestObserved = false; + + // 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); + loggedASRequestObserved = false; } else { - std::cout << "Failed to build acceleration structures, will retry next frame" << std::endl; + // 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 @@ -1492,11 +1534,31 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // 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); // Safe point pre-pass: ensure descriptor sets exist for all visible entities this frame // and initialize only binding 0 (UBO) for the current frame if not already done. { + watchdogProgressLabel.store("Render: per-entity descriptor cold-init", std::memory_order_relaxed); + // If we're in the loading overlay's Finalizing phase, estimate how many entities + // will be processed so we can update a meaningful progress fraction. + size_t totalToInit = 0; + if (IsLoading() && GetLoadingPhase() == LoadingPhase::Finalizing) + { + for (Entity *entity : entities) + { + if (!entity || !entity->IsActive()) + continue; + if (!entity->GetComponent()) + continue; + if (entityResources.find(entity) == entityResources.end()) + continue; + ++totalToInit; + } + SetLoadingPhaseProgress(0.0f); + } uint32_t entityProcessCount = 0; for (Entity *entity : entities) { @@ -1509,11 +1571,17 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca if (entityIt == entityResources.end()) continue; - // Update watchdog every 100 entities to prevent false hang detection during heavy descriptor creation + // Update watchdog more frequently during heavy descriptor set initialization + // large scenes can have thousands of entities needing cold-init. entityProcessCount++; - if (entityProcessCount % 100 == 0) + watchdogProgressIndex.store(entityProcessCount, std::memory_order_relaxed); + if (entityProcessCount % 10 == 0) { lastFrameUpdateTime.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + if (IsLoading() && GetLoadingPhase() == LoadingPhase::Finalizing && totalToInit > 0) + { + SetLoadingPhaseProgress(static_cast(entityProcessCount) / static_cast(totalToInit)); + } } // Determine a reasonable base texture path for initial descriptor writes @@ -1533,6 +1601,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // Ensure ONLY binding 0 (UBO) is written for the CURRENT frame's PBR set once. // Avoid touching image bindings here to keep per-frame descriptor churn minimal. + watchdogProgressLabel.store("ColdInit: PBR UBO", std::memory_order_relaxed); updateDescriptorSetsForFrame(entity, texPath, /*usePBR=*/true, @@ -1542,6 +1611,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // Basic/Phong pipeline also needs a per-frame UBO init at the safe point. // Descriptor sets for non-current frames are allocated but may not be initialized yet. + watchdogProgressLabel.store("ColdInit: Basic UBO", std::memory_order_relaxed); updateDescriptorSetsForFrame(entity, texPath, /*usePBR=*/false, @@ -1559,6 +1629,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } if (!entityIt->second.pbrImagesWritten[currentFrame]) { + watchdogProgressLabel.store("ColdInit: PBR images", std::memory_order_relaxed); updateDescriptorSetsForFrame(entity, texPath, /*usePBR=*/true, @@ -1574,6 +1645,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } if (!entityIt->second.basicImagesWritten[currentFrame]) { + watchdogProgressLabel.store("ColdInit: Basic images", std::memory_order_relaxed); updateDescriptorSetsForFrame(entity, texPath, /*usePBR=*/false, @@ -1583,6 +1655,27 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca entityIt->second.basicImagesWritten[currentFrame] = true; } } + watchdogProgressLabel.store("Render: after per-entity descriptor cold-init", std::memory_order_relaxed); + if (IsLoading() && GetLoadingPhase() == LoadingPhase::Finalizing) + { + SetLoadingPhaseProgress(1.0f); + } + } + + // 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 @@ -1590,14 +1683,22 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // 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 @@ -1611,6 +1712,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca 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 @@ -1654,10 +1756,12 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca vk::ResultValue result{{}, 0}; try { + watchdogProgressLabel.store("Render: acquireNextImage", std::memory_order_relaxed); result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[acquireSemaphoreIndex]); } 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); @@ -1676,6 +1780,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } imageIndex = result.value; + watchdogProgressLabel.store("Render: acquired swapchain image", std::memory_order_relaxed); if (result.result == vk::Result::eErrorOutOfDateKHR || result.result == vk::Result::eSuboptimalKHR || framebufferResized.load(std::memory_order_relaxed)) { @@ -1753,7 +1858,9 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca { if (*tlasStructure.handle) { + watchdogProgressLabel.store("Render: updateRayQueryDescriptorSets", std::memory_order_relaxed); updateRayQueryDescriptorSets(currentFrame, entities); + watchdogProgressLabel.store("Render: after updateRayQueryDescriptorSets", std::memory_order_relaxed); } } @@ -1849,15 +1956,8 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca refitTopLevelAS(entities); } - // Update descriptors for this frame. If it fails (e.g., stale/invalid sets), skip ray query safely. - if (!updateRayQueryDescriptorSets(currentFrame, entities)) - { - std::cerr << "Ray Query descriptor update failed; skipping ray query this frame\n"; - } - else - { - // Bind ray query compute pipeline - commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eCompute, *rayQueryPipeline); + // Bind ray query compute pipeline + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eCompute, *rayQueryPipeline); // Bind descriptor set commandBuffers[currentFrame].bindDescriptorSets( @@ -2037,7 +2137,6 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // Ray query rendering complete - set flag to skip rasterization code path rayQueryRenderedThisFrame = true; - } } } diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 546f03f9..76e88e76 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1114,6 +1114,31 @@ bool Renderer::createMeshResources(MeshComponent *meshComponent, bool deferUploa 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 != nullptr && res.vertexBuffer != nullptr) || + (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr)) + { + if (res.vertexBufferSizeBytes > 0 && res.stagingVertexBuffer != nullptr && res.vertexBuffer != nullptr) + { + copyBuffer(res.stagingVertexBuffer, res.vertexBuffer, res.vertexBufferSizeBytes); + res.stagingVertexBuffer = nullptr; + res.stagingVertexBufferMemory = nullptr; + res.vertexBufferSizeBytes = 0; + } + if (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr) + { + copyBuffer(res.stagingIndexBuffer, res.indexBuffer, res.indexBufferSizeBytes); + res.stagingIndexBuffer = nullptr; + res.stagingIndexBufferMemory = nullptr; + res.indexBufferSizeBytes = 0; + } + } + } return true; } @@ -1210,6 +1235,13 @@ 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()) @@ -1373,9 +1405,15 @@ bool Renderer::createDescriptorPool() // Create descriptor sets bool Renderer::createDescriptorSets(Entity *entity, 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); - std::shared_lock texLock(textureResourcesMutex); try { auto entityIt = entityResources.find(entity); @@ -1464,9 +1502,16 @@ bool Renderer::createDescriptorSets(Entity *entity, const std::string &texturePa for (int j = 0; j < 5; j++) { const auto resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); - auto textureIt = textureResources.find(resolvedBindingPath); - TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - imageInfos[j] = {.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + 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]}); } @@ -1534,10 +1579,17 @@ bool Renderer::createDescriptorSets(Entity *entity, const std::string &texturePa } else { // Basic Pipeline - // ... (this part remains the same) - auto textureIt = textureResources.find(resolvedTexturePath); - TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - vk::DescriptorImageInfo imageInfo{.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + // ... (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}}; @@ -1635,20 +1687,36 @@ bool Renderer::preAllocateEntityResources(Entity *entity) // 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) { @@ -1656,9 +1724,11 @@ bool Renderer::preAllocateEntityResourcesBatch(const std::vector &enti } // Ensure local AABB is available for debug/probes + watchdogProgressLabel.store("Batch: RecomputeLocalAABB", std::memory_order_relaxed); meshComponent->RecomputeLocalAABB(); - if (!createMeshResources(meshComponent, true)) + 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; @@ -1673,7 +1743,7 @@ bool Renderer::preAllocateEntityResourcesBatch(const std::vector &enti MeshResources &res = it->second; // Only schedule meshes that still have staged data pending upload - if (res.vertexBufferSizeBytes > 0 && res.indexBufferSizeBytes > 0) + if (res.vertexBufferSizeBytes > 0 || res.indexBufferSizeBytes > 0) { meshesNeedingUpload.push_back(meshComponent); } @@ -1682,23 +1752,41 @@ bool Renderer::preAllocateEntityResourcesBatch(const std::vector &enti // --- 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): " @@ -1717,6 +1805,7 @@ bool Renderer::preAllocateEntityResourcesBatch(const std::vector &enti } } + 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): " @@ -1724,6 +1813,7 @@ bool Renderer::preAllocateEntityResourcesBatch(const std::vector &enti 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): " @@ -1788,6 +1878,8 @@ 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; { @@ -1803,6 +1895,7 @@ void Renderer::ProcessPendingEntityPreallocations() } // 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()); @@ -1819,27 +1912,119 @@ void Renderer::ProcessPendingEntityPreallocations() 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 - for (Entity *e : toRecreateInstances) + // Process instance buffer recreations. + // Wait for GPU idle ONCE before processing the batch to safely destroy old buffers. + if (!toRecreateInstances.empty()) { - if (!e || !e->IsActive()) - continue; - if (!recreateInstanceBuffer(e)) + 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; + fencesToWait.reserve(inFlightFences.size() > 0 ? (inFlightFences.size() - 1) : 0); + for (uint32_t i = 0; i < static_cast(inFlightFences.size()); ++i) + { + if (i == currentFrame) + continue; + if (inFlightFences[i] != nullptr && *inFlightFences[i] != vk::Fence{}) + { + fencesToWait.push_back(*inFlightFences[i]); + } + } + if (!fencesToWait.empty()) { - std::cerr << "Warning: failed to recreate instance buffer for entity " << e->GetName() << std::endl; + (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 != nullptr && *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 = nullptr; + res.stagingVertexBufferMemory = nullptr; + res.vertexBufferSizeBytes = 0; + res.stagingIndexBuffer = nullptr; + res.stagingIndexBufferMemory = 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; { @@ -1849,11 +2034,26 @@ void Renderer::ProcessPendingMeshUploads() 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; @@ -1870,65 +2070,126 @@ void Renderer::ProcessPendingMeshUploads() 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}); - for (auto *meshComponent : needsCopy) + // 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 != nullptr && *uploadsTimeline != vk::Semaphore{}) && !forceSynchronous; + if (canSignalTimeline) { - auto it = meshResources.find(meshComponent); - if (it == meshResources.end()) - continue; - MeshResources &res = it->second; - if (res.vertexBufferSizeBytes > 0 && res.stagingVertexBuffer != nullptr && res.vertexBuffer != nullptr) + 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) { - vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; - cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources &res = it->second; + if (res.vertexBufferSizeBytes > 0 && res.stagingVertexBuffer != nullptr && res.vertexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; + cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + } + if (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; + cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + } } - if (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr) + + cb.end(); + + uint64_t signalValue = 0; { - vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; - cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + 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}); - cb.end(); + for (auto *meshComponent : needsCopy) + { + auto it = meshResources.find(meshComponent); + if (it == meshResources.end()) + continue; + MeshResources &res = it->second; + if (res.vertexBufferSizeBytes > 0 && res.stagingVertexBuffer != nullptr && res.vertexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.vertexBufferSizeBytes}; + cb.copyBuffer(*res.stagingVertexBuffer, *res.vertexBuffer, region); + } + if (res.indexBufferSizeBytes > 0 && res.stagingIndexBuffer != nullptr && res.indexBuffer != nullptr) + { + vk::BufferCopy region{.srcOffset = 0, .dstOffset = 0, .size = res.indexBufferSizeBytes}; + cb.copyBuffer(*res.stagingIndexBuffer, *res.indexBuffer, region); + } + } - // Submit and wait on the GRAPHICS queue (single-threaded via queueMutex) - vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*cb}; - vk::raii::Fence fence(device, vk::FenceCreateInfo{}); - { - std::lock_guard lock(queueMutex); - graphicsQueue.submit(submitInfo, *fence); - } - (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + cb.end(); - // Clear staging once copies are complete - for (auto *meshComponent : needsCopy) - { - auto it = meshResources.find(meshComponent); - if (it == meshResources.end()) - continue; - MeshResources &res = it->second; - res.stagingVertexBuffer = nullptr; - res.stagingVertexBufferMemory = nullptr; - res.vertexBufferSizeBytes = 0; - res.stagingIndexBuffer = nullptr; - res.stagingIndexBufferMemory = nullptr; - res.indexBufferSizeBytes = 0; - } - - // 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. - // This is safe at the render‑thread safe point and avoids partial TLAS builds. - asDevOverrideAllowRebuild = true; // allow rebuild even if frozen - RequestAccelerationStructureBuild("uploads completed"); + 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 = nullptr; + res.stagingVertexBufferMemory = nullptr; + res.vertexBufferSizeBytes = 0; + res.stagingIndexBuffer = nullptr; + res.stagingIndexBufferMemory = nullptr; + res.indexBufferSizeBytes = 0; + } + + asDevOverrideAllowRebuild = true; + RequestAccelerationStructureBuild("uploads completed"); + } } // Recreate instance buffer for an entity (e.g., after clearing instances for animation) @@ -1971,11 +2232,8 @@ bool Renderer::recreateInstanceBuffer(Entity *entity) std::cerr << "Warning: Instance buffer allocation is not mapped" << std::endl; } - // Wait for GPU to finish using the old instance buffer before destroying it. - // External synchronization required (VVL): serialize against queue submits/present. - WaitIdle(); - - // Replace the old instance buffer with the new one + // 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; @@ -2242,7 +2500,7 @@ void Renderer::copyBuffer(vk::raii::Buffer &srcBuffer, vk::raii::Buffer &dstBuff std::lock_guard lock(queueMutex); transferQueue.submit(submitInfo, *fence); } - [[maybe_unused]] auto fenceResult2 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); } catch (const std::exception &e) { @@ -2486,12 +2744,12 @@ void Renderer::transitionImageLayout(vk::Image image, vk::Format format, vk::Ima submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &*uploadsTimeline; } - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &*commandBuffer; - graphicsQueue.submit(submitInfo, *fence); - } - [[maybe_unused]] auto fenceResult3 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); - } + 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; @@ -2559,7 +2817,7 @@ void Renderer::copyBufferToImage(vk::Buffer buffer, vk::Image image, uint32_t wi submitInfo.pCommandBuffers = &*commandBuffer; graphicsQueue.submit(submitInfo, *fence); } - [[maybe_unused]] auto fenceResult4 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); } catch (const std::exception &e) { @@ -3375,9 +3633,9 @@ void Renderer::StartUploadsWorker(size_t workerCount) submit.pCommandBuffers = &*cb; transferQueue.submit(submit, *fence); } - device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); - // Perf accounting for the batch + // Perf accounting for the batch uint64_t batchBytes = 0; for (auto &it : items) batchBytes += static_cast(it.w) * it.h * 4ull; @@ -3525,6 +3783,15 @@ void Renderer::OnTextureUploaded(const std::string &textureId) 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) @@ -3561,7 +3828,8 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, descriptorRefreshPending.store(true, std::memory_order_relaxed); return true; } - std::shared_lock texLock(textureResourcesMutex); + // IMPORTANT: Do NOT hold `textureResourcesMutex` across this function. + // We may call `ResolveTextureId()` (which also locks it), and `std::shared_mutex` is not recursive. auto entityIt = entityResources.find(entity); if (entityIt == entityResources.end()) return false; @@ -3643,10 +3911,17 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, for (int j = 0; j < 5; ++j) { - const auto resolvedBindingPath = ResolveTextureId(pbrTexturePaths[j]); - auto textureIt = textureResources.find(resolvedBindingPath); - TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - imageInfos[j] = {.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + 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. @@ -3668,10 +3943,17 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, } else { - const std::string resolvedTexturePath = ResolveTextureId(texturePath); - auto textureIt = textureResources.find(resolvedTexturePath); - TextureResources *texRes = (textureIt != textureResources.end()) ? &textureIt->second : &defaultTextureResources; - vk::DescriptorImageInfo imageInfo{.sampler = *texRes->textureSampler, .imageView = *texRes->textureImageView, .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + 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 = { @@ -3732,10 +4014,18 @@ void Renderer::ProcessDirtyDescriptorsForFrame(uint32_t frameIndex) } } + 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; @@ -3796,8 +4086,15 @@ void Renderer::ProcessPendingTextureJobs(uint32_t maxJobs, 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))) @@ -3971,7 +4268,7 @@ void Renderer::uploadImageFromStaging(vk::Buffer st transferQueue.submit(submit, *fence); } - [[maybe_unused]] auto fenceResult5 = device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); // Perf accounting auto t1 = std::chrono::steady_clock::now(); @@ -4079,5 +4376,5 @@ void Renderer::generateMipmaps(vk::Image image, submit.pCommandBuffers = &*cb; graphicsQueue.submit(submit, *fence); } - (void) device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + (void) waitForFencesSafe(*fence, VK_TRUE); } diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp index bd98327a..4daa029d 100644 --- a/attachments/simple_engine/renderer_utils.cpp +++ b/attachments/simple_engine/renderer_utils.cpp @@ -336,8 +336,72 @@ vk::Extent2D Renderer::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabi // 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 != nullptr && *fence != vk::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 != nullptr && *uploadsTimeline != vk::Semaphore{}) + { + 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/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 76c82ccb..2dc77b0f 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -89,6 +89,25 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, try { + const auto loadStart = std::chrono::steady_clock::now(); + std::cout << "[Loading] Begin: " << modelPath << std::endl; + + // Loading large scenes can produce tens of thousands of entities. + // Avoid per-entity stdout spam (very slow on Windows consoles) and instead + // keep counters + print occasional summaries. + size_t physicsBodiesQueued = 0; + size_t physicsBodiesSkipped = 0; + size_t physicsNoGeometry = 0; + auto maybeLogPhysicsProgress = [&]() { + const size_t total = physicsBodiesQueued + physicsBodiesSkipped + physicsNoGeometry; + // Log infrequently to keep visibility without tanking load time. + if (total > 0 && (total % 5000u) == 0u) + { + std::cout << "[Loading] Physics bodies: queued=" << physicsBodiesQueued + << ", skipped=" << physicsBodiesSkipped + << ", noGeometry=" << physicsNoGeometry << std::endl; + } + }; // Load the complete GLTF model with all textures and lighting on the main thread Model *loadedModel = modelLoader->LoadGLTF(modelPath); if (!loadedModel) @@ -196,8 +215,21 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, std::vector geometryEntities; geometryEntities.reserve(materialMeshes.size()); - for (const auto &materialMesh : materialMeshes) + // Phase: Physics (queue colliders / rigid bodies). This is CPU-side work that can + // take noticeable time even after textures have finished scheduling. + if (renderer) { + renderer->SetLoadingPhase(Renderer::LoadingPhase::Physics); + renderer->SetLoadingPhaseProgress(0.0f); + } + + for (size_t meshIdx = 0; meshIdx < materialMeshes.size(); ++meshIdx) + { + const auto &materialMesh = materialMeshes[meshIdx]; + if (renderer && (meshIdx % 64u) == 0u) + { + renderer->SetLoadingPhaseProgress(materialMeshes.empty() ? 0.0f : (static_cast(meshIdx) / static_cast(materialMeshes.size()))); + } // Create an entity name based on model and material std::string entityName = modelName + "_Material_" + std::to_string(materialMesh.materialIndex) + "_" + materialMesh.materialName; @@ -376,17 +408,19 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, 0.15f, // restitution 0.5f // friction ); - std::cout << "Queued physics body for near-ground geometry entity: " << entityName << std::endl; + ++physicsBodiesQueued; + maybeLogPhysicsProgress(); } else { - std::cout << "Skipped physics body for high/remote entity: " << entityName - << " (minY=" << minWS.y << ")" << std::endl; + ++physicsBodiesSkipped; + maybeLogPhysicsProgress(); } } else { - std::cerr << "Skipping physics body for entity (no geometry): " << entityName << std::endl; + ++physicsNoGeometry; + maybeLogPhysicsProgress(); } } } @@ -395,6 +429,10 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; } } + if (renderer) + { + renderer->SetLoadingPhaseProgress(1.0f); + } // Pre-allocate Vulkan resources for all geometry entities in a single batched pass if (!geometryEntities.empty()) @@ -405,6 +443,17 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, renderer->EnqueueEntityPreallocationBatch(geometryEntities); } + // Final loading summary (useful for profiling, low-noise) + std::cout << "[Loading] Physics bodies summary: queued=" << physicsBodiesQueued + << ", skipped=" << physicsBodiesSkipped + << ", noGeometry=" << physicsNoGeometry << std::endl; + + const auto loadEnd = std::chrono::steady_clock::now(); + const auto loadMs = std::chrono::duration_cast(loadEnd - loadStart).count(); + const auto loadSecs = static_cast(loadMs) / 1000.0; + const bool loadFastOk = loadSecs <= 60.0; + std::cout << "[Loading] End: " << modelPath << " in " << loadSecs << "s" << (loadFastOk ? "" : " (SLOW)") << std::endl; + // Set up animations if the model has any const std::vector &animations = loadedModel->GetAnimations(); std::cout << "[Animation] Model has " << animations.size() << " animation(s)" << std::flush << std::endl; @@ -608,13 +657,15 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, return false; } - // Request acceleration structure build at next safe frame point - // Don't build here in background thread to avoid threading issues with command pools - if (renderer && renderer->GetRayQueryEnabled() && renderer->GetAccelerationStructureEnabled()) - { - std::cout << "Requesting acceleration structure build for loaded scene..." << std::endl; - renderer->RequestAccelerationStructureBuild(); - } + // Request acceleration structure build at next safe frame point + // Don't build here in background thread to avoid threading issues with command pools + if (renderer && renderer->GetRayQueryEnabled() && renderer->GetAccelerationStructureEnabled()) + { + renderer->SetLoadingPhase(Renderer::LoadingPhase::AccelerationStructures); + renderer->SetLoadingPhaseProgress(0.0f); + std::cout << "Requesting acceleration structure build for loaded scene..." << std::endl; + renderer->RequestAccelerationStructureBuild(); + } return true; } From b5a8882551a3d069d7f66b612eb11a0f90faa749 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 01:27:28 -0800 Subject: [PATCH 09/24] Add SimpleEngine CI workflow for automated builds - Sets up CI for Linux and Windows platforms. - Includes Vulkan SDK setup and Linux prerequisites installation. - Adds vcpkg manifest integration for dependency management. --- .github/workflows/simple_engine_ci.yml | 88 ++++++++++++++++++++++++++ attachments/simple_engine/vcpkg.json | 13 +++- 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/simple_engine_ci.yml diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml new file mode 100644 index 00000000..5e8b065e --- /dev/null +++ b/.github/workflows/simple_engine_ci.yml @@ -0,0 +1,88 @@ +name: SimpleEngine CI + +on: + push: + paths: + - 'attachments/simple_engine/**' + - '.github/workflows/simple_engine_ci.yml' + pull_request: + paths: + - 'attachments/simple_engine/**' + - '.github/workflows/simple_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/simple_engine + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up MSVC dev environment + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set up Ninja + if: runner.os == 'Windows' + uses: seanmiddleditch/gha-setup-ninja@v5 + + - name: Install Vulkan SDK (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (Test-Path "C:\VulkanSDK") { + Write-Host "Using cached Vulkan SDK" + } else { + Write-Host "Downloading Vulkan SDK installer..." + choco install -y aria2 + 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" + Write-Host "Installing Vulkan SDK (minimal components)..." + Start-Process -FilePath "$env:TEMP\vulkan-sdk.exe" -ArgumentList "--accept-licenses --default-answer --confirm-command install --components VulkanRT,VulkanSDK64,VulkanDXC,VulkanTools" -Wait -NoNewWindow + } + + $vulkanPath = "" + if (Test-Path "C:\VulkanSDK") { + $vulkanPath = Get-ChildItem "C:\VulkanSDK" | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty FullName + } + if (-not $vulkanPath) { + throw "Vulkan SDK not found after install" + } + + "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 + "Vulkan_INCLUDE_DIR=$vulkanPath\Include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "Vulkan_LIBRARY=$vulkanPath\Lib\vulkan-1.lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install Linux prerequisites + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libvulkan-dev \ + libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev + + - name: Set up vcpkg (manifest) + id: runvcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgJsonGlob: 'attachments/simple_engine/vcpkg.json' + runVcpkgInstall: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE="${{ steps.runvcpkg.outputs.vcpkgRootDir }}/scripts/buildsystems/vcpkg.cmake" + + - name: Build + run: cmake --build build --target SimpleEngine --parallel 4 diff --git a/attachments/simple_engine/vcpkg.json b/attachments/simple_engine/vcpkg.json index d246de0d..7c2809c5 100644 --- a/attachments/simple_engine/vcpkg.json +++ b/attachments/simple_engine/vcpkg.json @@ -2,11 +2,18 @@ "name": "vulkan-game-engine-tutorial", "version": "1.0.0", "dependencies": [ - "glfw3", + { + "name": "glfw3", + "platform": "!android" + }, "glm", "openal-soft", - "ktx", + { + "name": "ktx", + "features": [ "vulkan" ] + }, "tinygltf", "nlohmann-json" - ] + ], + "builtin-baseline": "2f84ffcfe5db02bf2d3b8377663cac7debaeef84" } From 0fb8c9d67e695ec8ea99cca47ebcca29785b9ca5 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 02:07:45 -0800 Subject: [PATCH 10/24] Enhance CI workflow for Vulkan SDK setup and dependency management - Improved Vulkan SDK installation for both Windows and Linux, including error handling and streamlined processes. - Replaced direct vcpkg calls with platform-specific dependency install scripts. - Added configuration for Ninja builds and automated test execution. --- .github/workflows/simple_engine_ci.yml | 106 +++++++++++++++++++++---- 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 5e8b065e..a34f40c3 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -40,14 +40,24 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | + $ErrorActionPreference = 'Stop' + + if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + throw "Chocolatey is required on windows-latest runners" + } + if (Test-Path "C:\VulkanSDK") { - Write-Host "Using cached Vulkan SDK" + Write-Host "Using existing Vulkan SDK at C:\VulkanSDK" } else { Write-Host "Downloading Vulkan SDK installer..." 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" - Write-Host "Installing Vulkan SDK (minimal components)..." - Start-Process -FilePath "$env:TEMP\vulkan-sdk.exe" -ArgumentList "--accept-licenses --default-answer --confirm-command install --components VulkanRT,VulkanSDK64,VulkanDXC,VulkanTools" -Wait -NoNewWindow + + Write-Host "Installing Vulkan SDK (silent, default feature set)..." + # NOTE: Do not pass --components here. LunarG has changed component IDs over time, + # and specifying them can cause 'Component(s) not found' failures. + Start-Process -FilePath $installer -ArgumentList "--accept-licenses --default-answer --confirm-command install" -Wait -NoNewWindow } $vulkanPath = "" @@ -64,25 +74,93 @@ jobs: "Vulkan_INCLUDE_DIR=$vulkanPath\Include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "Vulkan_LIBRARY=$vulkanPath\Lib\vulkan-1.lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: Install Linux prerequisites + - name: Install Vulkan SDK (Linux) if: runner.os == 'Linux' + shell: bash run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y wget gnupg ca-certificates + wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - + + # Pick the correct LunarG repo list for the runner's Ubuntu codename. + codename="" + if [ -f /etc/os-release ]; then + . /etc/os-release + codename="${VERSION_CODENAME:-}" + fi + if [ -z "$codename" ] && command -v lsb_release >/dev/null 2>&1; then + codename="$(lsb_release -sc)" + fi + if [ -z "$codename" ]; then + codename="jammy" + fi + + listUrl="https://packages.lunarg.com/vulkan/lunarg-vulkan-${codename}.list" + listPath="/etc/apt/sources.list.d/lunarg-vulkan-${codename}.list" + if ! sudo wget -qO "$listPath" "$listUrl"; then + echo "Warning: failed to fetch ${listUrl}; falling back to jammy list" + codename="jammy" + listUrl="https://packages.lunarg.com/vulkan/lunarg-vulkan-${codename}.list" + listPath="/etc/apt/sources.list.d/lunarg-vulkan-${codename}.list" + sudo wget -qO "$listPath" "$listUrl" + fi + sudo apt-get update - sudo apt-get install -y ninja-build libvulkan-dev \ - libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev + sudo apt-get install -y vulkan-sdk + + # We configure with Ninja on Linux; ensure it's available. + sudo apt-get install -y ninja-build + + # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + 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 + } + Push-Location $vcpkgRoot + .\bootstrap-vcpkg.bat + Pop-Location - - name: Set up vcpkg (manifest) - id: runvcpkg - uses: lukka/run-vcpkg@v11 - with: - vcpkgJsonGlob: 'attachments/simple_engine/vcpkg.json' - runVcpkgInstall: true + "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: Configure + - 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: | + chmod +x ./install_dependencies_linux.sh + ./install_dependencies_linux.sh + + - name: Configure (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: > + cmake -S . -B build -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE="$env:CMAKE_TOOLCHAIN_FILE" + + - name: Configure (Linux) + if: runner.os == 'Linux' + shell: bash run: > cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - -DCMAKE_TOOLCHAIN_FILE="${{ steps.runvcpkg.outputs.vcpkgRootDir }}/scripts/buildsystems/vcpkg.cmake" - name: Build run: cmake --build build --target SimpleEngine --parallel 4 + + - name: Test + run: ctest --test-dir build --output-on-failure From e1a1d13a1277ffc0bccc7098e08c825afeadf54c Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 02:11:05 -0800 Subject: [PATCH 11/24] Remove Vulkan SDK installation from Linux dependency script --- attachments/simple_engine/install_dependencies_linux.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/attachments/simple_engine/install_dependencies_linux.sh b/attachments/simple_engine/install_dependencies_linux.sh index e82daebc..b0e85b4d 100755 --- a/attachments/simple_engine/install_dependencies_linux.sh +++ b/attachments/simple_engine/install_dependencies_linux.sh @@ -39,13 +39,6 @@ install_ubuntu_debian() { # Install build essentials sudo apt install -y build-essential cmake git - # Install Vulkan SDK - echo "Installing Vulkan SDK..." - wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - - sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list https://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list - sudo apt update - sudo apt install -y vulkan-sdk - # Install other dependencies sudo apt install -y \ libglfw3-dev \ From 150d14941aba1a69d8232c8392f0ebb2770e6c7e Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 02:47:40 -0800 Subject: [PATCH 12/24] Streamline Vulkan SDK setup and dependency handling - Transitioned CI workflow to use LunarG's official Vulkan SDK tarball for Linux. - Removed platform-specific Vulkan SDK package installations in favor of consistent tarball handling. - Added new scripts for building and installing tinygltf and KTX dependencies from source. - Updated Linux dependency scripts to standardize package installations across distributions. - Improved build instructions to use Ninja by default. --- .github/workflows/simple_engine_ci.yml | 50 ++--- .../install_dependencies_linux.sh | 208 ++++++++++++------ 2 files changed, 163 insertions(+), 95 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index a34f40c3..3b1a8fd7 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -80,37 +80,29 @@ jobs: run: | set -euo pipefail sudo apt-get update - sudo apt-get install -y wget gnupg ca-certificates - wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - - - # Pick the correct LunarG repo list for the runner's Ubuntu codename. - codename="" - if [ -f /etc/os-release ]; then - . /etc/os-release - codename="${VERSION_CODENAME:-}" + sudo apt-get install -y curl ca-certificates xz-utils + + echo "Downloading Vulkan SDK from LunarG..." + # Use the official LunarG download endpoint (latest Linux tarball). + # We avoid distro packages so this matches what students are expected to do. + SDK_TGZ="${RUNNER_TEMP}/vulkansdk-linux.tar.xz" + curl -L --fail -o "$SDK_TGZ" "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz" + + SDK_DIR="${RUNNER_TEMP}/VulkanSDK" + rm -rf "$SDK_DIR" + mkdir -p "$SDK_DIR" + tar -xJf "$SDK_TGZ" -C "$SDK_DIR" + + # The tarball extracts into a versioned subdirectory. + VULKAN_SDK_PATH="$(find "$SDK_DIR" -maxdepth 1 -type d -name '1.*' | sort -r | head -n 1)" + if [ -z "${VULKAN_SDK_PATH:-}" ]; then + echo "Failed to locate extracted Vulkan SDK directory under $SDK_DIR" >&2 + exit 1 fi - if [ -z "$codename" ] && command -v lsb_release >/dev/null 2>&1; then - codename="$(lsb_release -sc)" - fi - if [ -z "$codename" ]; then - codename="jammy" - fi - - listUrl="https://packages.lunarg.com/vulkan/lunarg-vulkan-${codename}.list" - listPath="/etc/apt/sources.list.d/lunarg-vulkan-${codename}.list" - if ! sudo wget -qO "$listPath" "$listUrl"; then - echo "Warning: failed to fetch ${listUrl}; falling back to jammy list" - codename="jammy" - listUrl="https://packages.lunarg.com/vulkan/lunarg-vulkan-${codename}.list" - listPath="/etc/apt/sources.list.d/lunarg-vulkan-${codename}.list" - sudo wget -qO "$listPath" "$listUrl" - fi - - sudo apt-get update - sudo apt-get install -y vulkan-sdk - # We configure with Ninja on Linux; ensure it's available. - sudo apt-get install -y ninja-build + echo "VULKAN_SDK=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" + echo "$VULKAN_SDK_PATH/bin" >> "$GITHUB_PATH" + echo "CMAKE_PREFIX_PATH=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. - name: Bootstrap vcpkg (Windows) diff --git a/attachments/simple_engine/install_dependencies_linux.sh b/attachments/simple_engine/install_dependencies_linux.sh index b0e85b4d..29c0cde5 100755 --- a/attachments/simple_engine/install_dependencies_linux.sh +++ b/attachments/simple_engine/install_dependencies_linux.sh @@ -30,80 +30,158 @@ fi echo "Detected OS: $OS $VER" # Function to install dependencies on Ubuntu/Debian +require_vulkan_headers() { + if [ -n "${VULKAN_SDK:-}" ] && [ -f "${VULKAN_SDK}/include/vulkan/vulkan.h" ]; then + return 0 + fi + if [ -f "/usr/include/vulkan/vulkan.h" ]; then + return 0 + fi + echo "" + echo "Vulkan SDK (or Vulkan development headers) not found." + echo "Install the Vulkan SDK from LunarG, then re-run this script." + echo "https://vulkan.lunarg.com/" + exit 1 +} + +build_and_install_tinygltf() { + if [ -f "/usr/local/lib/cmake/tinygltf/tinygltfConfig.cmake" ] || [ -f "/usr/lib/cmake/tinygltf/tinygltfConfig.cmake" ]; then + return 0 + fi + local workRoot="$1" + echo "Installing tinygltf (from source)..." + rm -rf "${workRoot}/tinygltf" "${workRoot}/tinygltf-build" + git clone --depth 1 https://github.com/syoyo/tinygltf.git "${workRoot}/tinygltf" + cmake -S "${workRoot}/tinygltf" -B "${workRoot}/tinygltf-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DTINYGLTF_BUILD_LOADER_EXAMPLE=OFF \ + -DTINYGLTF_BUILD_GL_EXAMPLES=OFF \ + -DTINYGLTF_BUILD_STB_IMAGE=ON + cmake --build "${workRoot}/tinygltf-build" --parallel + sudo cmake --install "${workRoot}/tinygltf-build" +} + +build_and_install_ktx() { + if [ -f "/usr/local/lib/cmake/KTX/KTXConfig.cmake" ] || [ -f "/usr/lib/cmake/KTX/KTXConfig.cmake" ]; then + return 0 + fi + require_vulkan_headers + local workRoot="$1" + echo "Installing KTX-Software (from source)..." + rm -rf "${workRoot}/KTX-Software" "${workRoot}/KTX-Software-build" + git clone --depth 1 --branch v4.3.2 https://github.com/KhronosGroup/KTX-Software.git "${workRoot}/KTX-Software" + cmake -S "${workRoot}/KTX-Software" -B "${workRoot}/KTX-Software-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DKTX_FEATURE_TESTS=OFF \ + -DKTX_FEATURE_TOOLS=OFF \ + -DKTX_FEATURE_VULKAN=ON + cmake --build "${workRoot}/KTX-Software-build" --parallel + sudo cmake --install "${workRoot}/KTX-Software-build" +} + install_ubuntu_debian() { - echo "Installing dependencies for Ubuntu/Debian..." - - # Update package list - sudo apt update - - # Install build essentials - sudo apt install -y build-essential cmake git - - # Install other dependencies - sudo apt install -y \ - libglfw3-dev \ - libglm-dev \ - libopenal-dev \ - libktx-dev - - # Install Slang compiler (for shader compilation) - echo "Installing Slang compiler..." - if [ ! -f /usr/local/bin/slangc ]; then - SLANG_VERSION="2024.1.21" - wget "https://github.com/shader-slang/slang/releases/download/v${SLANG_VERSION}/slang-${SLANG_VERSION}-linux-x86_64.tar.gz" - tar -xzf "slang-${SLANG_VERSION}-linux-x86_64.tar.gz" - sudo cp slang/bin/slangc /usr/local/bin/ - sudo chmod +x /usr/local/bin/slangc - rm -rf slang "slang-${SLANG_VERSION}-linux-x86_64.tar.gz" - fi + echo "Installing dependencies for Ubuntu/Debian..." + + sudo apt-get update + + sudo apt-get install -y \ + build-essential \ + cmake \ + git \ + ninja-build \ + pkg-config \ + ca-certificates \ + curl \ + zip \ + unzip \ + tar \ + libglfw3-dev \ + libglm-dev \ + libopenal-dev \ + nlohmann-json3-dev \ + libx11-dev \ + libxrandr-dev \ + libxinerama-dev \ + libxcursor-dev \ + libxi-dev \ + zlib1g-dev \ + libpng-dev \ + libzstd-dev + + local workRoot + workRoot="${HOME}/.cache/simple_engine_deps" + mkdir -p "${workRoot}" + + build_and_install_tinygltf "${workRoot}" + build_and_install_ktx "${workRoot}" + + echo "" + echo "Note: This script does not install Vulkan or slangc." + echo "Install the Vulkan SDK from LunarG (it includes slangc) if you need shader compilation." } # Function to install dependencies on Fedora/RHEL/CentOS install_fedora_rhel() { echo "Installing dependencies for Fedora/RHEL/CentOS..." - # Install build essentials - sudo dnf install -y gcc gcc-c++ cmake git - - # Install Vulkan SDK - echo "Installing Vulkan SDK..." - sudo dnf install -y vulkan-devel vulkan-tools - - # Install other dependencies - sudo dnf install -y \ - glfw-devel \ - glm-devel \ - openal-soft-devel - - # Note: Some packages might need to be built from source on RHEL/CentOS - echo "Note: Some dependencies (libktx, tinygltf) may need to be built from source" - echo "Please refer to the project documentation for manual installation instructions" + sudo dnf install -y \ + gcc \ + gcc-c++ \ + cmake \ + git \ + ninja-build \ + pkgconf-pkg-config \ + glfw-devel \ + glm-devel \ + openal-soft-devel \ + nlohmann-json-devel \ + zlib-devel \ + libpng-devel \ + zstd-devel + + local workRoot + workRoot="${HOME}/.cache/simple_engine_deps" + mkdir -p "${workRoot}" + + build_and_install_tinygltf "${workRoot}" + build_and_install_ktx "${workRoot}" + + echo "" + echo "Note: This script does not install Vulkan or slangc." } # Function to install dependencies on Arch Linux install_arch() { echo "Installing dependencies for Arch Linux..." - # Update package database - sudo pacman -Sy - - # Install build essentials - sudo pacman -S --noconfirm base-devel cmake git - - # Install dependencies - sudo pacman -S --noconfirm \ - vulkan-devel \ - glfw-wayland \ - glm \ - openal - - # Install AUR packages (requires yay or another AUR helper) - if command -v yay &> /dev/null; then - yay -S --noconfirm libktx - else - echo "Note: Please install yay or another AUR helper to install libktx packages" - echo "Alternatively, build these dependencies from source" - fi + sudo pacman -Sy + + + sudo pacman -S --noconfirm \ + base-devel \ + cmake \ + git \ + ninja \ + pkgconf \ + glfw \ + glm \ + openal \ + nlohmann-json \ + zlib \ + libpng \ + zstd + + local workRoot + workRoot="${HOME}/.cache/simple_engine_deps" + mkdir -p "${workRoot}" + + build_and_install_tinygltf "${workRoot}" + build_and_install_ktx "${workRoot}" + + echo "" + echo "Note: This script does not install Vulkan or slangc." } # Install dependencies based on detected OS @@ -138,9 +216,7 @@ echo "Dependencies installation completed!" echo "" echo "To build the Simple Game Engine:" echo "1. cd to the simple_engine directory" -echo "2. mkdir build && cd build" -echo "3. cmake .." -echo "4. make -j$(nproc)" +echo "2. cmake -S . -B build -G Ninja" +echo "3. cmake --build build --target SimpleEngine --parallel $(nproc)" echo "" -echo "Or use the provided CMake build command:" -echo "cmake --build cmake-build-debug --target SimpleEngine -j 10" +echo "If you have Vulkan SDK installed, shader compilation uses slangc from the SDK automatically." From 4d9cc3e0a4025f6d530a4e95f9ba66a3b559e6c5 Mon Sep 17 00:00:00 2001 From: gpx1000 Date: Fri, 19 Dec 2025 02:50:53 -0800 Subject: [PATCH 13/24] Improve Vulkan SDK download resilience in CI workflow - Added multiple fallback URLs for Vulkan SDK download from LunarG. - Implemented error handling to ensure CI exits gracefully if all downloads fail. --- .github/workflows/simple_engine_ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 3b1a8fd7..22735c3e 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -84,9 +84,25 @@ jobs: echo "Downloading Vulkan SDK from LunarG..." # Use the official LunarG download endpoint (latest Linux tarball). - # We avoid distro packages so this matches what students are expected to do. SDK_TGZ="${RUNNER_TEMP}/vulkansdk-linux.tar.xz" - curl -L --fail -o "$SDK_TGZ" "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz" + + download_ok=0 + for url in \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz?Human=true" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz?Human=true" + do + echo "Attempting: $url" + if curl -L --fail -o "$SDK_TGZ" "$url"; then + download_ok=1 + break + fi + done + if [ "$download_ok" -ne 1 ]; then + echo "Failed to download Vulkan SDK from LunarG (all endpoints returned non-200)." >&2 + exit 1 + fi SDK_DIR="${RUNNER_TEMP}/VulkanSDK" rm -rf "$SDK_DIR" From 65eac4f91bf9c2410d2f11f863154ec380f466e4 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 03:57:40 -0800 Subject: [PATCH 14/24] Update KTX dependency to version 4.4.2 in CMake configuration. --- attachments/simple_engine/CMake/FindKTX.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attachments/simple_engine/CMake/FindKTX.cmake b/attachments/simple_engine/CMake/FindKTX.cmake index ac6971a2..c8c1c22c 100644 --- a/attachments/simple_engine/CMake/FindKTX.cmake +++ b/attachments/simple_engine/CMake/FindKTX.cmake @@ -87,7 +87,7 @@ else() FetchContent_Declare( ktx GIT_REPOSITORY https://github.com/KhronosGroup/KTX-Software.git - GIT_TAG v4.3.1 # Use a specific tag for stability + GIT_TAG v4.4.2 # Use a specific tag for stability ) # Set options to minimize build time and dependencies From 82c8c5db66ead25166c0f4c3321f460004a0ff74 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 03:58:09 -0800 Subject: [PATCH 15/24] Remove unused "builtin-baseline" field from vcpkg.json. --- attachments/simple_engine/vcpkg.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/attachments/simple_engine/vcpkg.json b/attachments/simple_engine/vcpkg.json index 7c2809c5..0ec18a26 100644 --- a/attachments/simple_engine/vcpkg.json +++ b/attachments/simple_engine/vcpkg.json @@ -14,6 +14,5 @@ }, "tinygltf", "nlohmann-json" - ], - "builtin-baseline": "2f84ffcfe5db02bf2d3b8377663cac7debaeef84" + ] } From 376c7ea99f27e00a08dd95daa1f2ca6138f0498b Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 04:16:08 -0800 Subject: [PATCH 16/24] Add Clang and Ninja toolchain setup for Linux CI builds. --- .github/workflows/simple_engine_ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 22735c3e..70344ebf 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -28,6 +28,22 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install Clang + Ninja (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y clang ninja-build + + - 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 @@ -166,6 +182,8 @@ jobs: run: > cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_COMPILER=clang + -DCMAKE_CXX_COMPILER=clang++ - name: Build run: cmake --build build --target SimpleEngine --parallel 4 From 24ce77c439fd84e99b8a728080e1b59fda94b150 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 04:24:24 -0800 Subject: [PATCH 17/24] Update Vulkan SDK setup in CI workflow for Linux builds. Enhance Vulkan SDK configuration by dynamically handling sysroot paths, setting explicit include and library directories, and refining CMake arguments, ensuring better compatibility and diagnostics. --- .github/workflows/simple_engine_ci.yml | 56 ++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 70344ebf..7c4e27fb 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -132,9 +132,39 @@ jobs: exit 1 fi - echo "VULKAN_SDK=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" - echo "$VULKAN_SDK_PATH/bin" >> "$GITHUB_PATH" - echo "CMAKE_PREFIX_PATH=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" + # Most LunarG tarballs have the actual sysroot under `x86_64/`. + # We want `VULKAN_SDK` to point at the directory that contains `include/`, `lib/`, `bin/`. + SDK_SYSROOT="$VULKAN_SDK_PATH" + if [ -d "$VULKAN_SDK_PATH/x86_64" ]; then + SDK_SYSROOT="$VULKAN_SDK_PATH/x86_64" + fi + + echo "VULKAN_SDK=$SDK_SYSROOT" >> "$GITHUB_ENV" + echo "$SDK_SYSROOT/bin" >> "$GITHUB_PATH" + echo "CMAKE_PREFIX_PATH=$SDK_SYSROOT" >> "$GITHUB_ENV" + + # Force CMake's FindVulkan to use the SDK headers/libs instead of runner system headers. + echo "Vulkan_INCLUDE_DIR=$SDK_SYSROOT/include" >> "$GITHUB_ENV" + if [ -f "$SDK_SYSROOT/lib/libvulkan.so" ]; then + echo "Vulkan_LIBRARY=$SDK_SYSROOT/lib/libvulkan.so" >> "$GITHUB_ENV" + elif [ -f "$SDK_SYSROOT/lib/libvulkan.so.1" ]; then + echo "Vulkan_LIBRARY=$SDK_SYSROOT/lib/libvulkan.so.1" >> "$GITHUB_ENV" + fi + + - name: Vulkan SDK diagnostics (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + echo "VULKAN_SDK=$VULKAN_SDK" + echo "Vulkan_INCLUDE_DIR=${Vulkan_INCLUDE_DIR:-}" + echo "Vulkan_LIBRARY=${Vulkan_LIBRARY:-}" + if [ -f "${VULKAN_SDK}/include/vulkan/vulkan.hpp" ]; then + echo "Using SDK vulkan.hpp at: ${VULKAN_SDK}/include/vulkan/vulkan.hpp" + head -n 5 "${VULKAN_SDK}/include/vulkan/vulkan.hpp" || true + else + echo "WARNING: SDK vulkan.hpp not found under ${VULKAN_SDK}/include/vulkan/vulkan.hpp" >&2 + fi # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. - name: Bootstrap vcpkg (Windows) @@ -179,11 +209,21 @@ jobs: - name: Configure (Linux) if: runner.os == 'Linux' shell: bash - run: > - cmake -S . -B build -G Ninja - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_C_COMPILER=clang - -DCMAKE_CXX_COMPILER=clang++ + run: | + set -euo pipefail + extra_args=() + if [ -n "${Vulkan_INCLUDE_DIR:-}" ]; then + extra_args+=("-DVulkan_INCLUDE_DIR=${Vulkan_INCLUDE_DIR}") + fi + if [ -n "${Vulkan_LIBRARY:-}" ]; then + extra_args+=("-DVulkan_LIBRARY=${Vulkan_LIBRARY}") + fi + + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + "${extra_args[@]}" - name: Build run: cmake --build build --target SimpleEngine --parallel 4 From ba1cbd795f68b5b3e4638fe4226d22de62c16e49 Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 17:03:13 -0800 Subject: [PATCH 18/24] Refine Vulkan SDK setup in Linux CI workflow: introduce sysroot separation, enhance environment variable handling, improve tool detection, and add support for slangc in CMake configuration. --- .github/workflows/simple_engine_ci.yml | 40 +++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 7c4e27fb..6aaa70a1 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -132,15 +132,28 @@ jobs: exit 1 fi - # Most LunarG tarballs have the actual sysroot under `x86_64/`. - # We want `VULKAN_SDK` to point at the directory that contains `include/`, `lib/`, `bin/`. + # LunarG Linux tarballs commonly have: + # - tools in: /bin (often includes slangc) + # - headers/libs in: /x86_64/{include,lib} + # We set VULKAN_SDK to the version root for tool discovery, and use VULKAN_SDK_SYSROOT + # to point CMake's FindVulkan at the correct include/lib paths. SDK_SYSROOT="$VULKAN_SDK_PATH" if [ -d "$VULKAN_SDK_PATH/x86_64" ]; then SDK_SYSROOT="$VULKAN_SDK_PATH/x86_64" fi - echo "VULKAN_SDK=$SDK_SYSROOT" >> "$GITHUB_ENV" - echo "$SDK_SYSROOT/bin" >> "$GITHUB_PATH" + echo "VULKAN_SDK=$VULKAN_SDK_PATH" >> "$GITHUB_ENV" + echo "VULKAN_SDK_SYSROOT=$SDK_SYSROOT" >> "$GITHUB_ENV" + + # Ensure SDK tools are on PATH regardless of whether they're in /bin or /x86_64/bin. + if [ -d "$VULKAN_SDK_PATH/bin" ]; then + echo "$VULKAN_SDK_PATH/bin" >> "$GITHUB_PATH" + fi + if [ -d "$SDK_SYSROOT/bin" ]; then + echo "$SDK_SYSROOT/bin" >> "$GITHUB_PATH" + fi + + # Prefer the sysroot for FindVulkan so we don't accidentally pick up runner system headers. echo "CMAKE_PREFIX_PATH=$SDK_SYSROOT" >> "$GITHUB_ENV" # Force CMake's FindVulkan to use the SDK headers/libs instead of runner system headers. @@ -151,19 +164,35 @@ jobs: echo "Vulkan_LIBRARY=$SDK_SYSROOT/lib/libvulkan.so.1" >> "$GITHUB_ENV" fi + # If slangc exists, capture its full path so we can pass it directly to CMake. + if command -v slangc >/dev/null 2>&1; then + echo "SLANGC_EXECUTABLE=$(command -v slangc)" >> "$GITHUB_ENV" + fi + - name: Vulkan SDK diagnostics (Linux) if: runner.os == 'Linux' shell: bash run: | set -euo pipefail echo "VULKAN_SDK=$VULKAN_SDK" + echo "VULKAN_SDK_SYSROOT=${VULKAN_SDK_SYSROOT:-}" echo "Vulkan_INCLUDE_DIR=${Vulkan_INCLUDE_DIR:-}" echo "Vulkan_LIBRARY=${Vulkan_LIBRARY:-}" + echo "SLANGC_EXECUTABLE=${SLANGC_EXECUTABLE:-}" + echo "PATH=$PATH" + echo "which slangc: $(command -v slangc || echo 'NOT FOUND')" + if command -v slangc >/dev/null 2>&1; then + slangc --version || slangc --help || true + fi if [ -f "${VULKAN_SDK}/include/vulkan/vulkan.hpp" ]; then echo "Using SDK vulkan.hpp at: ${VULKAN_SDK}/include/vulkan/vulkan.hpp" head -n 5 "${VULKAN_SDK}/include/vulkan/vulkan.hpp" || true else echo "WARNING: SDK vulkan.hpp not found under ${VULKAN_SDK}/include/vulkan/vulkan.hpp" >&2 + if [ -n "${VULKAN_SDK_SYSROOT:-}" ] && [ -f "${VULKAN_SDK_SYSROOT}/include/vulkan/vulkan.hpp" ]; then + echo "Found sysroot vulkan.hpp at: ${VULKAN_SDK_SYSROOT}/include/vulkan/vulkan.hpp" + head -n 5 "${VULKAN_SDK_SYSROOT}/include/vulkan/vulkan.hpp" || true + fi fi # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. @@ -218,6 +247,9 @@ jobs: if [ -n "${Vulkan_LIBRARY:-}" ]; then extra_args+=("-DVulkan_LIBRARY=${Vulkan_LIBRARY}") fi + if [ -n "${SLANGC_EXECUTABLE:-}" ]; then + extra_args+=("-DSLANGC_EXECUTABLE=${SLANGC_EXECUTABLE}") + fi cmake -S . -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ From f6d85bee6d07744134261837952662288fccefce Mon Sep 17 00:00:00 2001 From: swinston Date: Sat, 20 Dec 2025 17:19:17 -0800 Subject: [PATCH 19/24] Refine Linux CI workflow: add `spirv-tools` as a system fallback for `slangc`, enhance Vulkan SDK shared library handling, and improve environment diagnostics. --- .github/workflows/simple_engine_ci.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 6aaa70a1..083a4d74 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -96,7 +96,9 @@ jobs: run: | set -euo pipefail sudo apt-get update - sudo apt-get install -y curl ca-certificates xz-utils + # `spirv-opt` is required by `slangc` for SPIR-V downstream optimization on Linux. + # Prefer the SDK-provided tools when present, but install a system fallback to make CI robust. + sudo apt-get install -y curl ca-certificates xz-utils spirv-tools echo "Downloading Vulkan SDK from LunarG..." # Use the official LunarG download endpoint (latest Linux tarball). @@ -169,6 +171,17 @@ jobs: echo "SLANGC_EXECUTABLE=$(command -v slangc)" >> "$GITHUB_ENV" fi + # Ensure SDK-shipped shared libraries (e.g. slang-glslang-*) can be located at runtime. + # Also provide a small compatibility shim for Slang's attempt to load `pthread`. + compat_dir="${RUNNER_TEMP}/slang-compat" + mkdir -p "$compat_dir" + pthread_path="$(ldconfig -p | awk '/libpthread\.so\.0/{print $NF; exit 0}' || true)" + if [ -n "${pthread_path:-}" ] && [ -f "$pthread_path" ]; then + ln -sf "$pthread_path" "$compat_dir/libpthread.so" + fi + + echo "LD_LIBRARY_PATH=$compat_dir:${SDK_SYSROOT}/lib:${VULKAN_SDK_PATH}/lib:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV" + - name: Vulkan SDK diagnostics (Linux) if: runner.os == 'Linux' shell: bash @@ -179,8 +192,13 @@ jobs: echo "Vulkan_INCLUDE_DIR=${Vulkan_INCLUDE_DIR:-}" echo "Vulkan_LIBRARY=${Vulkan_LIBRARY:-}" echo "SLANGC_EXECUTABLE=${SLANGC_EXECUTABLE:-}" + echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}" echo "PATH=$PATH" echo "which slangc: $(command -v slangc || echo 'NOT FOUND')" + echo "which spirv-opt: $(command -v spirv-opt || echo 'NOT FOUND')" + if command -v spirv-opt >/dev/null 2>&1; then + spirv-opt --version || true + fi if command -v slangc >/dev/null 2>&1; then slangc --version || slangc --help || true fi From 6f6aaae10cefed9bc66bc8adabee6d337ba3d6f8 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 24 Dec 2025 00:11:05 -0800 Subject: [PATCH 20/24] Implement rasterization ray-query shadows and refine rendering pipelines: - Add support for ray-query shadows in raster fragment shaders using TLAS. - Introduce frustum and LOD culling for planar reflections and TLAS. - Enhance reflection handling with adjustable intensity and refined tone mapping. - Improve material and instance masking for ray-query shadows. - Optimize descriptor binding updates, reducing redundancy. - Refactor draw queue logic to separate opaque and blended entities for efficiency. - Extend RayQuery uniforms to support soft shadows and reflection customization. - Introduce rendering shadows with ray query to all pipelines. --- attachments/simple_engine/mesh_component.cpp | 84 ++++ attachments/simple_engine/mesh_component.h | 46 +- attachments/simple_engine/renderer.h | 49 +- .../simple_engine/renderer_pipelines.cpp | 9 +- .../simple_engine/renderer_ray_query.cpp | 76 ++- .../simple_engine/renderer_rendering.cpp | 276 +++++++---- .../simple_engine/renderer_resources.cpp | 160 ++++++- attachments/simple_engine/scene_loading.cpp | 8 +- .../simple_engine/shaders/common_types.slang | 9 +- .../simple_engine/shaders/composite.slang | 32 +- .../shaders/lighting_utils.slang | 22 +- attachments/simple_engine/shaders/pbr.slang | 237 +++++---- .../simple_engine/shaders/ray_query.slang | 451 +++++++++++++----- 13 files changed, 1081 insertions(+), 378 deletions(-) diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp index 39e8b198..57daa29c 100644 --- a/attachments/simple_engine/mesh_component.cpp +++ b/attachments/simple_engine/mesh_component.cpp @@ -17,6 +17,90 @@ #include "mesh_component.h" #include "model_loader.h" #include +#include + +// Helper to transform an AABB by a matrix +static void transformAABBLocal(const glm::mat4 &M, + const glm::vec3 &localMin, + const glm::vec3 &localMax, + glm::vec3 &outMin, + glm::vec3 &outMax) +{ + 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)); + 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; + + outMin = worldCenter - worldExtents; + outMax = worldCenter + worldExtents; +} + +void MeshComponent::RecomputeMeshAABB() +{ + if (meshAABBValid) + return; + + if (vertices.empty()) + { + meshAABBMin = glm::vec3(0.0f); + meshAABBMax = glm::vec3(0.0f); + meshAABBValid = false; + return; + } + glm::vec3 minB = vertices[0].position; + glm::vec3 maxB = vertices[0].position; + for (const auto &v : vertices) + { + minB = glm::min(minB, v.position); + maxB = glm::max(maxB, v.position); + } + meshAABBMin = minB; + meshAABBMax = maxB; + meshAABBValid = true; +} + +void MeshComponent::RecomputeLocalAABB() +{ + // First ensure base mesh AABB is up to date + RecomputeMeshAABB(); + + if (!meshAABBValid) + { + localAABBMin = glm::vec3(0.0f); + localAABBMax = glm::vec3(0.0f); + localAABBValid = false; + return; + } + + if (instances.empty()) + { + // No instances: local AABB is just the mesh AABB + localAABBMin = meshAABBMin; + localAABBMax = meshAABBMax; + localAABBValid = true; + } + else + { + // Union of all transformed instance AABBs + glm::vec3 fullMin(std::numeric_limits::max()); + glm::vec3 fullMax(-std::numeric_limits::max()); + + for (const auto &inst : instances) + { + glm::vec3 instMin, instMax; + transformAABBLocal(inst.modelMatrix, meshAABBMin, meshAABBMax, instMin, instMax); + fullMin = glm::min(fullMin, instMin); + fullMax = glm::max(fullMax, instMax); + } + + localAABBMin = fullMin; + localAABBMax = fullMax; + localAABBValid = true; + } +} // Most of the MeshComponent class implementation is in the header file // This file is mainly for any methods that might need additional implementation diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h index db03b535..6ba18d8e 100644 --- a/attachments/simple_engine/mesh_component.h +++ b/attachments/simple_engine/mesh_component.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -259,11 +260,16 @@ class MeshComponent final : public Component std::vector vertices; std::vector indices; - // Cached local-space AABB + // Cached local-space AABB (encompassing all instances) glm::vec3 localAABBMin{0.0f}; glm::vec3 localAABBMax{0.0f}; bool localAABBValid = false; + // Cached base mesh AABB (vertices only) + glm::vec3 meshAABBMin{0.0f}; + glm::vec3 meshAABBMax{0.0f}; + bool meshAABBValid = false; + // All PBR texture paths for this mesh std::string texturePath; // Primary texture path (baseColor) - kept for backward compatibility std::string baseColorTexturePath; // Base color (albedo) texture @@ -289,26 +295,8 @@ class MeshComponent final : public Component {} // Local AABB utilities - void RecomputeLocalAABB() - { - if (vertices.empty()) - { - localAABBMin = glm::vec3(0.0f); - localAABBMax = glm::vec3(0.0f); - localAABBValid = false; - return; - } - glm::vec3 minB = vertices[0].position; - glm::vec3 maxB = vertices[0].position; - for (const auto &v : vertices) - { - minB = glm::min(minB, v.position); - maxB = glm::max(maxB, v.position); - } - localAABBMin = minB; - localAABBMax = maxB; - localAABBValid = true; - } + void RecomputeLocalAABB(); + void RecomputeMeshAABB(); [[nodiscard]] bool HasLocalAABB() const { return localAABBValid; @@ -321,6 +309,14 @@ class MeshComponent final : public Component { return localAABBMax; } + [[nodiscard]] glm::vec3 GetBaseMeshAABBMin() const + { + return meshAABBMin; + } + [[nodiscard]] glm::vec3 GetBaseMeshAABBMax() const + { + return meshAABBMax; + } /** * @brief Set the vertices of the mesh. @@ -328,7 +324,9 @@ class MeshComponent final : public Component */ void SetVertices(const std::vector &newVertices) { - vertices = newVertices; + vertices = newVertices; + meshAABBValid = false; + localAABBValid = false; RecomputeLocalAABB(); } @@ -447,6 +445,7 @@ class MeshComponent final : public Component { instances.emplace_back(transform, materialIndex); isInstanced = instances.size() > 1; + RecomputeLocalAABB(); } /** @@ -457,6 +456,7 @@ class MeshComponent final : public Component { instances = newInstances; isInstanced = instances.size() > 1; + RecomputeLocalAABB(); } /** @@ -493,6 +493,7 @@ class MeshComponent final : public Component { instances.clear(); isInstanced = false; + RecomputeLocalAABB(); } /** @@ -506,6 +507,7 @@ class MeshComponent final : public Component if (index < instances.size()) { instances[index] = InstanceData(transform, materialIndex); + RecomputeLocalAABB(); } } diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h index ca191deb..1eb8fbda 100644 --- a/attachments/simple_engine/renderer.h +++ b/attachments/simple_engine/renderer.h @@ -93,6 +93,7 @@ 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 @@ -169,18 +170,23 @@ struct RayQueryUniformBufferObject 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; // reserved -}; - -static_assert(sizeof(RayQueryUniformBufferObject) == 272, "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); + // 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); @@ -195,6 +201,8 @@ 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. @@ -891,7 +899,7 @@ class Renderer bool buildAccelerationStructures(const std::vector &entities); // Refit/UPDATE the TLAS with latest entity transforms (no rebuild) - bool refitTopLevelAS(const std::vector &entities); + bool refitTopLevelAS(const std::vector &entities, CameraComponent *camera); /** * @brief Update ray query descriptor sets with current resources. @@ -1003,9 +1011,15 @@ class Renderer 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 + 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; @@ -1563,6 +1577,13 @@ class Renderer 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. diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp index dc7d7975..fc92a551 100644 --- a/attachments/simple_engine/renderer_pipelines.cpp +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -159,12 +159,19 @@ bool Renderer::createPBRDescriptorSetLayout() .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}}; // 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{}; + std::array bindingFlags{}; if (descriptorIndexingEnabled) { bindingFlags[0] = vk::DescriptorBindingFlagBits::eUpdateAfterBind | vk::DescriptorBindingFlagBits::eUpdateUnusedWhilePending; diff --git a/attachments/simple_engine/renderer_ray_query.cpp b/attachments/simple_engine/renderer_ray_query.cpp index a9706e0c..c2ca6331 100644 --- a/attachments/simple_engine/renderer_ray_query.cpp +++ b/attachments/simple_engine/renderer_ray_query.cpp @@ -19,6 +19,7 @@ #include "renderer.h" #include "transform_component.h" #include +#include #include #include #include @@ -598,8 +599,10 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities std::string matName = entityName.substr(numEnd + 1); if (Material *m = modelLoader->GetMaterial(matName)) { - // Only MASK requires candidate hits for alpha test. - forceNoOpaque = (m->alphaMode == "MASK"); + // Force non-opaque for any material that should not be treated as a fully-opaque occluder. + // - MASK: needs candidate hits so we can alpha-test in-shader + // - BLEND / glass / transmission: should not fully block shadow rays + forceNoOpaque = (m->alphaMode == "MASK") || (m->alphaMode == "BLEND") || m->isGlass || (m->transmissionFactor > 0.01f); } } } @@ -1147,7 +1150,7 @@ bool Renderer::buildAccelerationStructures(const std::vector &entities } } -bool Renderer::refitTopLevelAS(const std::vector &entities) +bool Renderer::refitTopLevelAS(const std::vector &entities, CameraComponent *camera) { try { @@ -1173,21 +1176,36 @@ bool Renderer::refitTopLevelAS(const std::vector &entities) } }; + // Optional culling parity with raster: mask TLAS instances using the same frustum + distance-LOD checks. + // Use the same culling toggles as the raster path. + const bool doFrustumCulling = enableFrustumCulling && camera; + const bool doDistanceLOD = enableDistanceLOD && camera; + FrustumPlanes frustum{}; + if (doFrustumCulling) + { + const glm::mat4 vp = camera->GetProjectionMatrix() * camera->GetViewMatrix(); + frustum = extractFrustumPlanes(vp); + } + const float camFovRad = camera ? glm::radians(camera->GetFieldOfView()) : glm::radians(60.0f); + for (uint32_t i = 0; i < tlasInstanceCount; ++i) { kickWatchdog(); const TlasInstanceRef &ref = tlasInstanceOrder[i]; Entity *entity = ref.entity; if (!entity || !entity->IsActive()) + { + instPtr[i].mask = 0u; continue; + } auto *transform = entity->GetComponent(); glm::mat4 entityModel = transform ? transform->GetModelMatrix() : glm::mat4(1.0f); // If this TLAS entry represents a MeshComponent instance, multiply by the instance's model glm::mat4 finalModel = entityModel; + auto *meshComp = entity->GetComponent(); if (ref.instanced) { - auto *meshComp = entity->GetComponent(); if (meshComp && ref.instanceIndex < meshComp->GetInstanceCount()) { const InstanceData &id = meshComp->GetInstance(ref.instanceIndex); @@ -1205,6 +1223,56 @@ bool Renderer::refitTopLevelAS(const std::vector &entities) } } instPtr[i].transform = vkTransform; + + // Apply culling via instance mask (mask=0 => skipped by ray queries with mask=0xFF). + uint32_t mask = 0xFFu; + if ((doFrustumCulling || doDistanceLOD) && meshComp && camera && meshComp->HasLocalAABB()) + { + bool visible = true; + glm::vec3 wmin{}, wmax{}; + transformAABB(finalModel, meshComp->GetBaseMeshAABBMin(), meshComp->GetBaseMeshAABBMax(), wmin, wmax); + + if (doFrustumCulling && !aabbIntersectsFrustum(wmin, wmax, frustum)) + { + visible = false; + } + + if (visible && doDistanceLOD) + { + // Match raster LOD heuristic (projected-size skip) + glm::vec3 center = 0.5f * (wmin + wmax); + glm::vec3 extents = 0.5f * (wmax - wmin); + float radius = glm::length(extents); + if (radius > 0.0f) + { + glm::vec4 centerVS4 = camera->GetViewMatrix() * glm::vec4(center, 1.0f); + float z = std::abs(centerVS4.z); + if (z > 1e-3f) + { + float pixelRadius = (radius * static_cast(swapChainExtent.height)) / + (z * 2.0f * std::tan(camFovRad * 0.5f)); + float pixelDiameter = pixelRadius * 2.0f; + + bool useBlended = false; + ensureEntityMaterialCache(entity); + auto it = entityResources.find(entity); + if (it != entityResources.end()) + { + useBlended = it->second.cachedIsBlended; + } + + float threshold = useBlended ? lodPixelThresholdTransparent : lodPixelThresholdOpaque; + if (pixelDiameter < threshold) + { + visible = false; + } + } + } + } + + mask = visible ? 0xFFu : 0u; + } + instPtr[i].mask = mask; } // Prepare UPDATE build info diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp index 90bc7014..87109eef 100644 --- a/attachments/simple_engine/renderer_rendering.cpp +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -18,15 +18,18 @@ #include "imgui_system.h" #include "model_loader.h" #include "renderer.h" +#include #include #include #include #include #include +#include #include #include #include #include +#include #include // ===================== Culling helpers implementation ===================== @@ -44,8 +47,8 @@ Renderer::FrustumPlanes Renderer::extractFrustumPlanes(const glm::mat4 &vp) fp.planes[2] = m[3] + m[1]; // Top : m[3] - m[1] fp.planes[3] = m[3] - m[1]; - // Near : m[3] + m[2] - fp.planes[4] = m[3] + m[2]; + // 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]; @@ -90,12 +93,16 @@ bool Renderer::aabbIntersectsFrustum(const glm::vec3 &worldMin, for (const auto &p : frustum.planes) { const glm::vec3 n(p.x, p.y, p.z); - // Choose positive vertex + // 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 (glm::dot(n, v) + p.w < 0.0f) + + // 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 } @@ -408,6 +415,9 @@ void Renderer::renderReflectionPass(vk::raii::CommandBuffer &cmd, cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrGraphicsPipeline); } + // Prepare frustum for mirrored view to allow culling + FrustumPlanes reflectFrustum = extractFrustumPlanes(currentReflectionVP); + // Render all entities with meshes (skip transparency; glass revisit later) for (Entity *entity : entities) { @@ -417,10 +427,29 @@ void Renderer::renderReflectionPass(vk::raii::CommandBuffer &cmd, if (!meshComponent) continue; + // Skip transparent/blended objects in planar reflections to avoid recursion + // and because the reflection pipeline uses opaque PBR (PSMain). + ensureEntityMaterialCache(entity); auto entityIt = entityResources.find(entity); if (entityIt == entityResources.end()) continue; + if (entityIt->second.cachedIsBlended) + continue; + + // Frustum culling for mirrored view + if (meshComponent->HasLocalAABB()) + { + auto *tc = entity->GetComponent(); + const glm::mat4 model = tc ? tc->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 + } + } + auto meshIt = meshResources.find(meshComponent); if (meshIt == meshResources.end()) continue; @@ -775,6 +804,7 @@ void Renderer::recreateSwapChain() resources.basicUboBindingWritten.clear(); resources.pbrImagesWritten.clear(); resources.basicImagesWritten.clear(); + resources.pbrFixedBindingsWritten.clear(); } } @@ -930,7 +960,7 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity *entity ubo.exposure = std::clamp(this->exposure, 0.2f, 4.0f); ubo.gamma = this->gamma; ubo.prefilteredCubeMipLevels = 0.0f; - ubo.scaleIBLAmbient = 0.25f; + ubo.scaleIBLAmbient = 1.0f; ubo.screenDimensions = glm::vec2(swapChainExtent.width, swapChainExtent.height); // Forward+ clustered parameters for fragment shader ubo.nearZ = camera ? camera->GetNearPlane() : 0.1f; @@ -945,7 +975,8 @@ void Renderer::updateUniformBufferInternal(uint32_t currentImage, Entity *entity ubo.padding0 = outputIsSRGB; // Padding fields no longer used for runtime debug toggles ubo.padding1 = 0.0f; - ubo.padding2 = 0.0f; + // `padding2`: raster ray-query shadows toggle (see `shaders/pbr.slang`) + ubo.padding2 = enableRasterRayQueryShadows ? 1.0f : 0.0f; // Planar reflections: set sampling flags/matrices for main pass; preserve reflectionPass if already set by caller if (ubo.reflectionPass != 1) @@ -1474,11 +1505,23 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca SetLoadingPhase(LoadingPhase::Finalizing); SetLoadingPhaseProgress(0.0f); } - loggedASRequestObserved = false; + loggedASRequestObserved = false; + + // 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) + // 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 && @@ -1891,8 +1934,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } } } - auto lightCountF = static_cast(lightsSubset.size()); - lastFrameLightCount = lightCountF; + lastFrameLightCount = static_cast(lightsSubset.size()); if (!lightsSubset.empty()) { updateLightStorageBuffer(currentFrame, lightsSubset); @@ -1953,7 +1995,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca { // If animation updated transforms this frame, refit TLAS instead of rebuilding // This prevents wiping TLAS contents to only animated instances - refitTopLevelAS(entities); + refitTopLevelAS(entities, camera); } // Bind ray query compute pipeline @@ -1988,8 +2030,8 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca 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 ~0.25 in the main pass; keep Ray Query consistent.) - ubo.scaleIBLAmbient = 0.25f; + // (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); @@ -2002,6 +2044,11 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca 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 @@ -2144,8 +2191,8 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca vk::raii::Pipeline *currentPipeline = nullptr; vk::raii::PipelineLayout *currentLayout = nullptr; + std::vector opaqueQueue; std::vector blendedQueue; - std::unordered_set blendedSet; // Incrementally process pending texture uploads on the main thread so that // all Vulkan submits happen from a single place while worker threads only @@ -2197,23 +2244,35 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca { 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) + if (ImGui::Combo("Mode", ¤tMode, modeNames, 2)) { - currentRenderMode = newMode; - std::cout << "Switched to " << modeNames[currentMode] << " mode\n"; + 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(); + 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)"); @@ -2258,6 +2317,16 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca } } + // 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)) @@ -2282,10 +2351,6 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca ImGui::Text("Reflection RT: %ux%u", rt.width, rt.height); } } - if (enablePlanarReflections) - { - ImGui::SliderFloat("Reflection intensity", &reflectionIntensity, 0.0f, 2.0f, "%.2f"); - } } // === RAY QUERY-SPECIFIC OPTIONS === @@ -2306,6 +2371,12 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca 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); @@ -2344,16 +2415,17 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca createTextureSampler(defaultTextureResources); } } - if (lastCullingVisibleCount + lastCullingCulledCount > 0) - { - ImGui::Text("Culling: visible=%u, culled=%u", lastCullingVisibleCount, lastCullingCulledCount); - } + if (lastCullingVisibleCount + lastCullingCulledCount > 0) + { + ImGui::Text("Culling: visible=%u, culled=%u", lastCullingVisibleCount, lastCullingCulledCount); + } - // Basic tone mapping controls - ImGui::Separator(); - ImGui::Text("Tone Mapping:"); - ImGui::SliderFloat("Exposure", &exposure, 0.1f, 4.0f, "%.2f"); - ImGui::SliderFloat("Gamma", &gamma, 1.6f, 2.6f, "%.2f"); + // 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(); } @@ -2362,12 +2434,22 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // Previously this always executed, but now we skip it when ray query mode successfully renders. if (!rayQueryRenderedThisFrame) { + // Force matrices to be up-to-date at the start of the frame + if (camera) + { + camera->ForceViewMatrixUpdate(); + } + // Prepare frustum once per frame FrustumPlanes frustum{}; const bool doCulling = enableFrustumCulling && camera; if (doCulling) { - const glm::mat4 vp = camera->GetProjectionMatrix() * camera->GetViewMatrix(); + // Use the same projection matrix as the shader (including the Vulkan Y-flip) + // to ensure culling perfectly matches the visual frustum. + glm::mat4 proj = camera->GetProjectionMatrix(); + proj[1][1] *= -1.0f; + const glm::mat4 vp = proj * camera->GetViewMatrix(); frustum = extractFrustumPlanes(vp); } @@ -2390,20 +2472,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca if (!meshComponent) continue; - // Frustum culling - if (doCulling && meshComponent->HasLocalAABB()) - { - auto *tc = entity->GetComponent(); - const glm::mat4 model = tc ? tc->GetModelMatrix() : glm::mat4(1.0f); - glm::vec3 wmin, wmax; - transformAABB(model, meshComponent->GetLocalAABBMin(), meshComponent->GetLocalAABBMax(), wmin, wmax); - if (!aabbIntersectsFrustum(wmin, wmax, frustum)) - { - lastCullingCulledCount++; - continue; // culled early - } - } - lastCullingVisibleCount++; + // Get cached material info bool useBlended = false; ensureEntityMaterialCache(entity); auto entityIt = entityResources.find(entity); @@ -2412,45 +2481,69 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca useBlended = entityIt->second.cachedIsBlended; } - // Ensure all entities are considered regardless of reflections setting. - // Previous diagnostic mode skipped non-glass when reflections were ON, which could - // result in frames with few/no draws and visible black flashes. We no longer filter here. - - // Distance-based LOD: approximate screen-space size of entity's bounds - if (enableDistanceLOD && camera && meshComponent && meshComponent->HasLocalAABB()) + // Perform culling if bounds are available + if (meshComponent->HasLocalAABB()) { - auto *tc = entity->GetComponent(); - const glm::mat4 model = tc ? tc->GetModelMatrix() : glm::mat4(1.0f); - glm::vec3 localMin = meshComponent->GetLocalAABBMin(); - glm::vec3 localMax = meshComponent->GetLocalAABBMax(); - // Compute world AABB bounds - glm::vec3 wmin, wmax; - transformAABB(model, localMin, localMax, wmin, wmax); - glm::vec3 center = 0.5f * (wmin + wmax); - glm::vec3 extents = 0.5f * (wmax - wmin); - float radius = glm::length(extents); - if (radius > 0.0f) + auto *tc = entity->GetComponent(); + 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)) { - glm::vec4 centerVS4 = camera->GetViewMatrix() * glm::vec4(center, 1.0f); - float z = std::abs(centerVS4.z); - if (z > 1e-3f) + lastCullingCulledCount++; + continue; + } + + // 2. Distance-based LOD + if (enableDistanceLOD && camera) + { + glm::vec3 center = 0.5f * (wmin + wmax); + glm::vec3 camPos = camera->GetPosition(); + + // If camera is inside the AABB, it's definitely not "too small" + 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 fov = glm::radians(camera->GetFieldOfView()); - float pixelRadius = (radius * static_cast(swapChainExtent.height)) / (z * 2.0f * std::tan(fov * 0.5f)); - float pixelDiameter = pixelRadius * 2.0f; - float threshold = useBlended ? lodPixelThresholdTransparent : lodPixelThresholdOpaque; - if (pixelDiameter < threshold) + glm::vec3 extents = 0.5f * (wmax - wmin); + float radius = glm::length(extents); + if (radius > 0.0f) { - // Too small to matter; skip adding to draw queues - continue; + // For huge sparse AABBs (like instanced groups), use the closest + // point distance to avoid culling when one instance is close but + // the group center is far. + 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 pixelRadius = (radius * static_cast(swapChainExtent.height)) / (z_eff * 2.0f * std::tan(fov * 0.5f)); + float pixelDiameter = pixelRadius * 2.0f; + float threshold = useBlended ? lodPixelThresholdTransparent : lodPixelThresholdOpaque; + if (pixelDiameter < threshold) + { + lastCullingCulledCount++; + continue; // too small + } } } } } + + lastCullingVisibleCount++; if (useBlended) { blendedQueue.push_back(entity); - blendedSet.insert(entity); + } + else + { + opaqueQueue.push_back(entity); } } @@ -2513,18 +2606,7 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca // Optional Forward+ depth pre-pass for opaque geometry if (useForwardPlus) { - // Build list of non-blended entities - std::vector opaqueEntities; - opaqueEntities.reserve(entities.size()); - for (Entity *entity : entities) - { - if (!entity || !entity->IsActive() || blendedSet.contains(entity)) - continue; - auto meshComponent = entity->GetComponent(); - if (!meshComponent) - continue; - opaqueEntities.push_back(entity); - } + const auto &opaqueEntities = opaqueQueue; if (!opaqueEntities.empty()) { @@ -2712,10 +2794,8 @@ void Renderer::Render(const std::vector &entities, CameraComponent *ca commandBuffers[currentFrame].setScissor(0, scissor); { uint32_t opaqueDrawsThisPass = 0; - for (Entity *entity : entities) + for (Entity *entity : opaqueQueue) { - if (!entity || !entity->IsActive() || (blendedSet.contains(entity))) - continue; auto meshComponent = entity->GetComponent(); if (!meshComponent) continue; diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp index 76e88e76..9a7a6998 100644 --- a/attachments/simple_engine/renderer_resources.cpp +++ b/attachments/simple_engine/renderer_resources.cpp @@ -1476,6 +1476,10 @@ bool Renderer::createDescriptorSets(Entity *entity, const std::string &texturePa // 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{}; // CRITICAL FIX: Buffer infos must remain in scope until updateDescriptorSets completes. // Previously these were declared inside nested scopes, causing dangling pointers // when descriptorWrites held pointers to them after they went out of scope. @@ -1572,6 +1576,37 @@ bool Renderer::createDescriptorSets(Entity *entity, const std::string &texturePa 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, {}); @@ -1647,6 +1682,7 @@ bool Renderer::preAllocateEntityResources(Entity *entity) 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); } } @@ -3149,6 +3185,14 @@ void Renderer::refreshPBRForwardPlusBindingsForFrame(uint32_t frameIndex) 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; @@ -3185,6 +3229,17 @@ void Renderer::refreshPBRForwardPlusBindingsForFrame(uint32_t frameIndex) } // 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); } if (!writes.empty()) @@ -3243,6 +3298,7 @@ bool Renderer::updateLightStorageBuffer(uint32_t frameIndex, const std::vectorsecond.basicUboBindingWritten.assign(MAX_FRAMES_IN_FLIGHT, false); } + if (entityIt->second.pbrFixedBindingsWritten.size() != MAX_FRAMES_IN_FLIGHT) + { + entityIt->second.pbrFixedBindingsWritten.assign(MAX_FRAMES_IN_FLIGHT, false); + } if (usePBR) { @@ -3877,6 +3937,86 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, // 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::DescriptorImageInfo reflInfo{}; + vk::AccelerationStructureKHR tlasHandleValue{}; + vk::WriteDescriptorSetAccelerationStructureKHR tlasInfo{}; + vk::WriteDescriptorSet tlasWrite{}; + const bool needFixedWrites = !entityIt->second.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 == nullptr)) + headersBuf = *f.tileHeaders; + if (!(f.tileLightIndices == nullptr)) + 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); + }; + // Optionally write only the UBO (binding 0) — used at safe point to initialize per-frame sets once if (uboOnly) { @@ -3884,11 +4024,20 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, if (!entityIt->second.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 (!entityIt->second.pbrUboBindingWritten[frameIndex]) { - std::lock_guard lk(descriptorMutex); - device.updateDescriptorSets(writes, {}); + entityIt->second.pbrUboBindingWritten[frameIndex] = true; + } + if (needFixedWrites) + { + entityIt->second.pbrFixedBindingsWritten[frameIndex] = true; } - entityIt->second.pbrUboBindingWritten[frameIndex] = true; } return true; } @@ -3931,10 +4080,15 @@ bool Renderer::updateDescriptorSetsForFrame(Entity *entity, 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) + { + entityIt->second.pbrFixedBindingsWritten[frameIndex] = true; + } // CRITICAL FIX: Only mark UBO as written if we actually wrote it (not during imagesOnly updates) if (!imagesOnly) { diff --git a/attachments/simple_engine/scene_loading.cpp b/attachments/simple_engine/scene_loading.cpp index 2dc77b0f..3eacc4d9 100644 --- a/attachments/simple_engine/scene_loading.cpp +++ b/attachments/simple_engine/scene_loading.cpp @@ -249,13 +249,7 @@ bool LoadGLTFModel(Engine *engine, const std::string &modelPath, if (materialMesh.GetInstanceCount() > 0) { - const std::vector &instances = materialMesh.instances; - for (const auto &instanceData : instances) - { - // Reconstruct the transformation matrix from InstanceData column vectors - glm::mat4 instanceMatrix = instanceData.getModelMatrix(); - mesh->AddInstance(instanceMatrix, static_cast(materialMesh.materialIndex)); - } + mesh->SetInstances(materialMesh.instances); } // Set ALL PBR texture paths for this material diff --git a/attachments/simple_engine/shaders/common_types.slang b/attachments/simple_engine/shaders/common_types.slang index a395b8d3..fa3a0e78 100644 --- a/attachments/simple_engine/shaders/common_types.slang +++ b/attachments/simple_engine/shaders/common_types.slang @@ -23,10 +23,11 @@ struct LightData { [[vk::offset(0)]] float4 position; [[vk::offset(16)]] float4 color; [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; - [[vk::offset(96)]] int lightType; - [[vk::offset(100)]] float range; - [[vk::offset(104)]] float innerConeAngle; - [[vk::offset(108)]] float outerConeAngle; + [[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 diff --git a/attachments/simple_engine/shaders/composite.slang b/attachments/simple_engine/shaders/composite.slang index 60138fe1..01f32133 100644 --- a/attachments/simple_engine/shaders/composite.slang +++ b/attachments/simple_engine/shaders/composite.slang @@ -17,6 +17,8 @@ // 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; @@ -47,34 +49,26 @@ struct Push { }; [[vk::push_constant]] Push pushConsts; -float3 tonemapReinhard(float3 x) -{ - return x / (1.0 + x); -} - -float3 applyExposure(float3 x, float exposure) -{ - return 1.0 - exp(-x * max(exposure, 0.0001)); -} - -float3 linearToGamma(float3 x, float gamma) -{ - float inv = (gamma > 0.0) ? (1.0 / gamma) : (1.0 / 2.2); - return pow(max(x, 0.0), inv); -} - // Export entrypoint for fragment stage [shader("fragment")] float4 PSMain(VSOut i) : SV_TARGET { float4 c = sceneColor.Sample(i.UV); float3 color = c.rgb; - // Simple exposure; optional reinhard if desired later - color = applyExposure(color, pushConsts.exposure); + + // 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 = linearToGamma(color, pushConsts.gamma); + color = pow(max(color, 0.0), float3(1.0 / pushConsts.gamma)); + } else { + color = saturate(color); } return float4(color, 1.0); } diff --git a/attachments/simple_engine/shaders/lighting_utils.slang b/attachments/simple_engine/shaders/lighting_utils.slang index 46f0975e..1ecb3fae 100644 --- a/attachments/simple_engine/shaders/lighting_utils.slang +++ b/attachments/simple_engine/shaders/lighting_utils.slang @@ -52,8 +52,26 @@ LightEvaluation evaluateLight(LightData light, float3 worldPos, float3 N) { result.radiance = light.color.rgb * att; result.valid = true; } else if (light.lightType == 0 || light.lightType == 2) { - // Point or spot light: inverse square falloff - result.radiance = light.color.rgb / max(d * d, 0.0001); + // 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; } } diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang index 8d2b6adf..c0f24a32 100644 --- a/attachments/simple_engine/shaders/pbr.slang +++ b/attachments/simple_engine/shaders/pbr.slang @@ -62,8 +62,47 @@ struct VSOutput { // 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; + [[vk::push_constant]] PushConstants material; +static const float RASTER_SHADOW_EPS = 0.002; + +// 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; + uint mask = 0xFF; + q.TraceRayInline( + tlas, + RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, + mask, + ray + ); + + int iter = 0; + while (q.Proceed() && iter < 256) + { + iter++; + if (q.CandidateType() == CANDIDATE_NON_OPAQUE_TRIANGLE) + { + // Skip non-opaque candidates (mask/blend/glass instances should be non-opaque). + // Do not commit. + } + } + return (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT); +} + // Vertex shader entry point [[shader("vertex")]] VSOutput VSMain(VSInput input) @@ -197,6 +236,7 @@ float4 PSMain(VSOutput input) : SV_TARGET 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); @@ -206,15 +246,31 @@ float4 PSMain(VSOutput input) : SV_TARGET 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); - float att = 1.0 / (1.0 + (d / r) * (d / r)); - radiance = light.color.rgb * att; + attenuation = 1.0 / (1.0 + (d / r) * (d / r)); } else { - // Punctual (not used currently) - radiance = light.color.rgb / max(d * d, 0.0001); + 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 @@ -222,6 +278,14 @@ float4 PSMain(VSOutput input) : SV_TARGET 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 + 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); @@ -231,8 +295,8 @@ float4 PSMain(VSOutput input) : SV_TARGET 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; - diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + specularLighting += spec * radiance * NdotL * visibility; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL * visibility; } } } @@ -244,6 +308,7 @@ float4 PSMain(VSOutput input) : SV_TARGET 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; @@ -251,16 +316,42 @@ float4 PSMain(VSOutput input) : SV_TARGET 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); - float att = 1.0 / (1.0 + (d / r) * (d / r)); - radiance = light.color.rgb * att; + attenuation = 1.0 / (1.0 + (d / r) * (d / r)); } else { - radiance = light.color.rgb / max(d * d, 0.0001); + 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) { + 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); @@ -270,13 +361,13 @@ float4 PSMain(VSOutput input) : SV_TARGET 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; - diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + specularLighting += spec * radiance * NdotL * visibility; + diffuseLighting += (kD * albedo / PI) * radiance * NdotL * visibility; } } } - float3 ambient = albedo * ao * (0.03 * ubo.scaleIBLAmbient); + float3 ambient = albedo * ao * (0.1 * ubo.scaleIBLAmbient); float3 opaqueLit = diffuseLighting + specularLighting + ambient + emissive; // --- 4. Final Color Assembly (opaque only; transmission handled in GlassPSMain) --- @@ -293,21 +384,7 @@ float4 PSMain(VSOutput input) : SV_TARGET // sampling here to avoid banding/aliasing and ensure user-requested behavior. // --- 5. Post-Processing --- - // Apply exposure and filmic tonemapping; apply gamma only if the swapchain is NOT sRGB. - color *= ubo.exposure; - - // Uncharted2 / Hable filmic tonemap, canonical form without the 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); - } - + // Output linear color for intermediate buffers (composite pass will tonemap) return float4(color, alphaOut); } @@ -327,11 +404,6 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET ? material.baseColorFactor : baseColorMap.Sample(uv) * material.baseColorFactor; - // Ambient occlusion - float ao = (material.occlusionTextureSet < 0) - ? 1.0 - : occlusionMap.Sample(uv).r; - // Emissive (same logic as PSMain) float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) @@ -350,42 +422,46 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET float3 G = normalize(input.GeometricNormal); float3 V = normalize(ubo.camPos.xyz - input.WorldPos); - // Base albedo used only for ambient tint and transmission tint + // Base albedo used for transmission tint float3 albedo = baseColor.rgb; - // Simple ambient term so glass has some base brightness even when the - // background is dark. For thin architectural glass we keep this very - // low so the glass itself does not look "filled in" or frosted; most - // of the perceived brightness should come from what you see *through* - // the glass rather than from the glass surface. - float3 ambient = albedo * (0.5 * ubo.scaleIBLAmbient); - - // Transmission factor from push constants - float T = clamp(material.transmissionFactor, 0.0, 1.0); - float T_eff = T; + // 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) { - // Reflections for glass - // Prefer planar reflection texture rendered from the mirrored view when enabled. - // Fallback: use the opaque scene color behind the glass (refraction/background approximation). - float3 refl = float3(0.0, 0.0, 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; - // Sample only if within the reflection RT if (uvP.x >= 0.0 && uvP.x <= 1.0 && uvP.y >= 0.0 && uvP.y <= 1.0) { refl = reflectionMap.Sample(uvP).rgb; } - } else { - // Project current position to screen for a stable background sample - float2 ndc = input.Position.xy / input.Position.w; - float2 uvR = ndc * 0.5 + 0.5; - uvR = clamp(uvR, float2(0.0, 0.0), float2(1.0, 1.0)); - refl = opaqueSceneColor.Sample(uvR).rgb; } // Stylized, stable glass: Use a tinted @@ -403,24 +479,22 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET float edge = pow(1.0 - NdotV, 3.0); float3 rimColor = lerp(clearColor, float3(1.0, 1.0, 1.0), 0.25); - // Blend clear glass and rim, scaled by transmission. Both are attenuated - // so that backgrounds remain visible through the glass. - float3 glassBody = clearColor * (T_eff * 0.5); - float3 rim = rimColor * (edge * T_eff * 0.4); + // Surface term: keep subtle so glass does not appear frosted. + float3 surfaceBase = emissive; + float3 surfaceTerm = surfaceBase * (1.0 - T_eff) * 0.12; - // Surface term: ambient + emissive, reduced as transmission grows and - // globally attenuated so it does not wash out the view through glass. - // Keep this subtle so glass does not appear frosted; for highly - // transmissive glass (T_eff close to 1) this term becomes very small. - float3 surfaceBase = ambient + emissive; - float3 surfaceTerm = surfaceBase * (1.0 - T_eff) * 0.15; + // 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; - color = 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.15 + (1.5 * F_avg2) * (1.0 - material.roughnessFactor)); + 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); @@ -433,22 +507,13 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET // opacity toward grazing angles. TransmissionFactor controls how // much of the underlying scene shows through overall. - // Base opacity driven primarily by transmission: highly - // transmissive glass stays very transparent. - float baseAlpha = lerp(0.08, 0.25, 1.0 - T_eff); - - // Edge boost: more opaque at grazing angles, scaled by Fresnel. - // Recompute a local Fresnel average for alpha since we’re in debug mode above. - float3 F_view_dbg = FresnelSchlick(NdotV, float3(0.04, 0.04, 0.04)); - float F_avg = (F_view_dbg.r + F_view_dbg.g + F_view_dbg.b) / 3.0; - float edgeFactor = pow(1.0 - NdotV, 2.0); - float edgeAlpha = F_avg * edgeFactor * 0.8; - - alphaOut = saturate(baseAlpha + edgeAlpha); - alphaOut = max(alphaOut, 0.02); + // 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 = ambient + emissive; + color = emissive; } // Simple Forward+ lighting for glass (additive), using per-tile lists. @@ -482,6 +547,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET 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; @@ -489,6 +555,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET 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)); @@ -500,6 +567,14 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET 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); @@ -510,7 +585,7 @@ float4 GlassPSMain(VSOutput input) : SV_TARGET 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; + color += ((kD * alb / PI) * radiance * NdotL * 0.6 + spec * radiance * NdotL * 0.8) * visibility; } } } diff --git a/attachments/simple_engine/shaders/ray_query.slang b/attachments/simple_engine/shaders/ray_query.slang index f43406fe..0b454150 100644 --- a/attachments/simple_engine/shaders/ray_query.slang +++ b/attachments/simple_engine/shaders/ray_query.slang @@ -107,7 +107,14 @@ struct RayQueryUniforms { [[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) - [[vk::offset(264)]] int _pad1; // reserved + // 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 @@ -159,7 +166,35 @@ struct HitInfo { uint materialIndex; // Resolved/clamped material index used }; -static const float RQ_RAY_EPS = 0.001; +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)); @@ -178,12 +213,9 @@ float computeTextureLOD(float3 worldPos, float roughness) { return max(lod, 0.0); } -float3 shadeWithSecondaryRays(float3 rayOrigin, float3 rayDir, HitInfo hit) { +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); - if (maxBounces <= 0) { - return base; - } float3 N = hit.normal; float3 V = normalize(-rayDir); @@ -200,25 +232,33 @@ float3 shadeWithSecondaryRays(float3 rayOrigin, float3 rayDir, HitInfo hit) { 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) { + glassTransmission = max(physicalTransmission, (1.0 - opacity)); + if (glassTransmission < 0.01) { + glassTransmission = 0.90; + } + } + float T = 0.0; if (ubo.enableRayQueryTransparency != 0 && !hit.isLiquid) { - // Do not force large T for glass; rely on authored/heuristic transmission. - // This avoids energy gain that can make glass appear opaque/overbright. - T = max(physicalTransmission, blendTransmission); + T = max(physicalTransmission, max(blendTransmission, 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; + float3 ro = hit.worldPos + hit.normal * RQ_RAY_EPS; float3 rd = normalize(reflect(rayDir, N)); - // Bias along the secondary ray direction to avoid self-intersection. - ro += rd * RQ_RAY_EPS; float3 last = skyColor(rd); for (int b = 0; b < maxBounces; ++b) { - HitInfo rh = traceRay(ro, rd, RQ_RAY_EPS, 10000.0); + 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; @@ -226,104 +266,116 @@ float3 shadeWithSecondaryRays(float3 rayOrigin, float3 rayDir, HitInfo hit) { // Next bounce float3 Nr = normalize(rh.normal); rd = normalize(reflect(rd, Nr)); - ro = rh.worldPos + rd * RQ_RAY_EPS; + ro = rh.worldPos + Nr * RQ_RAY_EPS; } - reflCol = last; + reflCol = last * ubo.reflectionIntensity; } - // Transmission ray: - // - Physical transmission / glass: thin refraction ray - // - Alpha BLEND with no physical transmission: straight-through ray (no refraction) - float3 thruCol = float3(0.0, 0.0, 0.0); + // Transmission ray (iterative loop to support multiple layers) + float3 thruCol = skyColor(rayDir); bool doThru = (T > 1e-4); if (doThru) { - float3 thruDir = rayDir; - - if (physicalTransmission > 1e-4 || hit.isGlass) { - // Thin refraction for glass/transmission - float3 Nn = N; - float eta = 1.0 / max(hit.ior, 1.0); - // Determine if we're entering or exiting based on ray direction vs normal - if (dot(rayDir, N) > 0.0) { - Nn = -N; - eta = max(hit.ior, 1.0); - } - float3 refrDir; - if (refract(rayDir, Nn, eta, refrDir)) { - thruDir = normalize(refrDir); - } else { - // Total internal reflection - doThru = false; + 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) 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; } - } - if (doThru) { - // We want the transmitted view of the scene BEYOND the glass exit surface, - // not the color of the glass backface itself. So: - // 1) Trace from just inside the entry point to find the exit surface on the same instance - // 2) If found, trace again from just beyond the exit point to sample the true background - // 3) If not found, fall back to the first trace color (behaves like thin surface) - - float3 startPos = hit.worldPos + thruDir * RQ_RAY_EPS; - HitInfo firstHit = traceRay(startPos, thruDir, RQ_RAY_EPS, 10000.0); - - // Attempt to find exit on the same instance (backface) - HitInfo exitHit = traceRay(startPos, thruDir, RQ_RAY_EPS, 10000.0); - bool haveExitSameInstance = exitHit.hit && (exitHit.instanceId == hit.instanceId); - - // Sample the scene beyond the exit if available, otherwise use firstHit - if (haveExitSameInstance) { - float3 exitPos = exitHit.worldPos + thruDir * RQ_RAY_EPS; - HitInfo beyond = traceRay(exitPos, thruDir, RQ_RAY_EPS, 10000.0); - thruCol = beyond.hit ? max(beyond.color, 0.0) : skyColor(thruDir); - } else { - thruCol = firstHit.hit ? max(firstHit.color, 0.0) : skyColor(thruDir); + float3 nextRd = curRd; + bool refracts = (curPhysT > 1e-4 || curHit.isGlass); + 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; + } } - // Base color tint (thin-glass default behavior) - if (hit.isGlass || physicalTransmission > 1e-4) { - thruCol *= clamp(hit.baseColor, 0.0, 1.0); + // 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); + // Volumetric absorption for THICK glass (skip for thin-walled) - if (ubo.enableThickGlass != 0 && !hit.thinWalled && (hit.isGlass || physicalTransmission > 1e-4)) { - float thickness = 0.0; - if (haveExitSameInstance) { - thickness = distance(exitHit.worldPos, hit.worldPos); - } else { - // Fallback small thickness if not watertight - thickness = 0.01; // 1 cm - } - // Clamp to avoid over-darkening from outliers - thickness = min(thickness, max(0.0, ubo.thicknessClamp)); - - if (thickness > 1e-6) { - // Convert absorptionColor to sigma_a using Beer–Lambert: C = exp(-sigma*D) => sigma = -ln(C)/D - uint mi = min(hit.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); - float3 Tvol = exp(-sigma_a * thickness); - thruCol *= saturate(Tvol); + 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); } } - // Refractive radiance compensation to mitigate perceived amplification. - // Our refract() call used eta = n1/n2. Scale transmitted radiance by - // min(1, (n2/n1)^2) = min(1, 1/(eta^2)) when ENTERING a denser medium. - // Do not amplify when exiting; clamp to 1.0 to avoid brightening. - { - // Recreate the n1/n2 used for refract(): if ray is entering (front-face), eta = n1/n2 = 1/ior; else eta = ior - bool entering = (dot(rayDir, N) < 0.0); - float eta_n1_over_n2 = entering ? (1.0 / max(hit.ior, 1.0)) : max(hit.ior, 1.0); + // 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_n1_over_n2 * eta_n1_over_n2, 1e-4); - float transScale = clamp(invEta2, 0.0, 1.0); - thruCol *= transScale; + 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; } } @@ -346,12 +398,13 @@ float3 shadeWithSecondaryRays(float3 rayOrigin, float3 rayDir, HitInfo hit) { return base * opacity + mixed * (1.0 - opacity); } - // Glass/transmission path: return the energy-conserving mixture directly. - return mixed; + // 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 = Fr * (1.0 - clamp(hit.roughness, 0.0, 1.0)); + float reflWeight = doRefl ? (Fr * (1.0 - clamp(hit.roughness, 0.0, 1.0))) : 0.0; return lerp(base, reflCol, reflWeight); } @@ -393,6 +446,65 @@ float computeBaseColorAlpha(MaterialData material, uint instIndex, uint primitiv 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) { @@ -407,7 +519,7 @@ bool refract(float3 I, float3 N, float eta, out float3 refracted) { } // Perform ray query and return hit information with proper vertex normals and material properties -HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { +HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax, inout uint rngState) { HitInfo result; result.hit = false; result.t = tMax; @@ -722,33 +834,116 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { } // --- Direct lighting (GGX) --- - float3 V = normalize(ubo.camPos.xyz - result.worldPos); + 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]; - float3 L; - float3 radiance; - if (light.lightType == 1) { - L = normalize(-light.position.xyz); - radiance = light.color.rgb; - } else { - float3 toLight = light.position.xyz - result.worldPos; - float d = length(toLight); - L = (d > 1e-5) ? (toLight / d) : float3(0, 0, 1); - 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; + + // 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 { - radiance = light.color.rgb / max(d * d, 0.0001); + // 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) { + + 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) { + float3 shadowOrigin = 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); @@ -757,13 +952,21 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { 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; - diffuseLighting += (kD * albedo / PI) * radiance * NdotL; + 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; } - float3 ambient = albedo * (ubo.scaleIBLAmbient) * ao; + // Avoid ambient "fill" on glass/transmissive surfaces; it can make them look opaque/frosted. + // Keep ambient for opaque materials to retain basic IBL/ambient parity. + bool treatAsTransmissive = (material.isGlass != 0) || (material.transmissionFactor > 0.01) || (material.alphaMode == 2); + 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; @@ -772,7 +975,8 @@ HitInfo traceRay(float3 origin, float3 direction, float tMin, float tMax) { result.transmission = material.transmissionFactor; result.isGlass = (material.isGlass != 0); result.alphaMode = material.alphaMode; - result.opacity = clamp(material.alpha, 0.0, 1.0); + // Keep texture-derived alpha/opacity (baseColor factor * baseColor texture). + result.opacity = clamp(baseColor.a, 0.0, 1.0); result.thinWalled = (material.thinWalled != 0); } @@ -820,10 +1024,11 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) float3 rayOrigin = ubo.camPos.xyz; float3 rayDir = normalize(farWorld - nearWorld); - HitInfo hit = traceRay(rayOrigin, rayDir, 0.0001, 10000.0); + 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); + float3 c = shadeWithSecondaryRays(rayOrigin, rayDir, hit, rngState); // Output linear HDR-ish color; composite pass will apply exposure/gamma. outputImage[pixelCoord] = float4(c, 1.0); From 447f32bdf2391112a837e2e124978feca95b4789 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 24 Dec 2025 00:55:52 -0800 Subject: [PATCH 21/24] Integrate ray-query shadows with Vulkan Ray Query, update linked chapters, and enhance CI with caching and toolchain improvements. --- .github/workflows/simple_engine_ci.yml | 183 +++++++++++++--- .../05_vulkan_integration.adoc | 4 +- .../Lighting_Materials/06_conclusion.adoc | 8 +- .../Lighting_Materials/07_shadows.adoc | 206 ++++++++++++++++++ .../Lighting_Materials/index.adoc | 1 + 5 files changed, 364 insertions(+), 38 deletions(-) create mode 100644 en/Building_a_Simple_Engine/Lighting_Materials/07_shadows.adoc diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 083a4d74..4e1f7258 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -28,13 +28,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Clang + Ninja (Linux) + - 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 + sudo apt-get install -y clang ninja-build ccache - name: Select Clang toolchain (Linux) if: runner.os == 'Linux' @@ -48,12 +48,39 @@ jobs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - - name: Set up Ninja + - name: Set up Ninja + sccache if: runner.os == 'Windows' - uses: seanmiddleditch/gha-setup-ninja@v5 + shell: pwsh + run: | + choco install -y ninja sccache + "SCCACHE_DIR=$env:LOCALAPPDATA\Mozilla\sccache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: Install Vulkan SDK (Windows) + - name: ccache (Linux) + if: runner.os == 'Linux' + uses: actions/cache@v4 + with: + path: ~/.cache/ccache + key: ${{ runner.os }}-ccache-${{ github.sha }} + restore-keys: ${{ runner.os }}-ccache- + + - name: sccache (Windows) if: runner.os == 'Windows' + uses: actions/cache@v4 + with: + path: ${{ env.SCCACHE_DIR }} + key: ${{ runner.os }}-sccache-${{ github.sha }} + restore-keys: ${{ runner.os }}-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' && steps.cache-vulkan-windows.outputs.cache-hit != 'true' shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -90,6 +117,14 @@ jobs: "Vulkan_INCLUDE_DIR=$vulkanPath\Include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "Vulkan_LIBRARY=$vulkanPath\Lib\vulkan-1.lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Cache Vulkan SDK (Linux) + if: runner.os == 'Linux' + id: cache-vulkan-linux + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/VulkanSDK + key: ${{ runner.os }}-vulkan-sdk + - name: Install Vulkan SDK (Linux) if: runner.os == 'Linux' shell: bash @@ -100,32 +135,37 @@ jobs: # Prefer the SDK-provided tools when present, but install a system fallback to make CI robust. sudo apt-get install -y curl ca-certificates xz-utils spirv-tools - echo "Downloading Vulkan SDK from LunarG..." - # Use the official LunarG download endpoint (latest Linux tarball). - SDK_TGZ="${RUNNER_TEMP}/vulkansdk-linux.tar.xz" - - download_ok=0 - for url in \ - "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz" \ - "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz" \ - "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz?Human=true" \ - "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz?Human=true" - do - echo "Attempting: $url" - if curl -L --fail -o "$SDK_TGZ" "$url"; then - download_ok=1 - break + SDK_DIR="${RUNNER_TEMP}/VulkanSDK" + + if [ "${{ steps.cache-vulkan-linux.outputs.cache-hit }}" != "true" ]; then + echo "Downloading Vulkan SDK from LunarG..." + # Use the official LunarG download endpoint (latest Linux tarball). + SDK_TGZ="${RUNNER_TEMP}/vulkansdk-linux.tar.xz" + + download_ok=0 + for url in \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz?Human=true" \ + "https://sdk.lunarg.com/sdk/download/latest/linux/vulkansdk-linux-x86_64.tar.xz?Human=true" + do + echo "Attempting: $url" + if curl -L --fail -o "$SDK_TGZ" "$url"; then + download_ok=1 + break + fi + done + if [ "$download_ok" -ne 1 ]; then + echo "Failed to download Vulkan SDK from LunarG (all endpoints returned non-200)." >&2 + exit 1 fi - done - if [ "$download_ok" -ne 1 ]; then - echo "Failed to download Vulkan SDK from LunarG (all endpoints returned non-200)." >&2 - exit 1 - fi - SDK_DIR="${RUNNER_TEMP}/VulkanSDK" - rm -rf "$SDK_DIR" - mkdir -p "$SDK_DIR" - tar -xJf "$SDK_TGZ" -C "$SDK_DIR" + rm -rf "$SDK_DIR" + mkdir -p "$SDK_DIR" + tar -xJf "$SDK_TGZ" -C "$SDK_DIR" + else + echo "Using cached Vulkan SDK from $SDK_DIR" + fi # The tarball extracts into a versioned subdirectory. VULKAN_SDK_PATH="$(find "$SDK_DIR" -maxdepth 1 -type d -name '1.*' | sort -r | head -n 1)" @@ -214,8 +254,18 @@ jobs: fi # Use the engine's dependency install scripts instead of calling vcpkg directly in CI. - - name: Bootstrap vcpkg (Windows) + - 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 }}-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' @@ -232,18 +282,72 @@ jobs: "$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" + "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=$env:RUNNER_TEMP\vcpkg-cache" | 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: Cache dependencies (Linux) + if: runner.os == 'Linux' + id: cache-deps-linux + uses: actions/cache@v4 + with: + path: | + /home/runner/.cache/simple_engine_deps + /home/runner/.local + key: ${{ runner.os }}-deps-v1 + - name: Install dependencies (Linux) if: runner.os == 'Linux' shell: bash run: | - chmod +x ./install_dependencies_linux.sh - ./install_dependencies_linux.sh + set -euo pipefail + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake git ninja-build pkg-config \ + ca-certificates curl zip unzip tar \ + libglfw3-dev libglm-dev libopenal-dev \ + nlohmann-json3-dev libx11-dev libxrandr-dev \ + libxinerama-dev libxcursor-dev libxi-dev \ + zlib1g-dev libpng-dev libzstd-dev + + WORK_ROOT="/home/runner/.cache/simple_engine_deps" + LOCAL_INSTALL="/home/runner/.local" + mkdir -p "${WORK_ROOT}" + + # build tinygltf + if [ ! -f "${LOCAL_INSTALL}/lib/cmake/tinygltf/tinygltfConfig.cmake" ]; then + git clone --depth 1 https://github.com/syoyo/tinygltf.git "${WORK_ROOT}/tinygltf" + cmake -S "${WORK_ROOT}/tinygltf" -B "${WORK_ROOT}/tinygltf-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DTINYGLTF_BUILD_LOADER_EXAMPLE=OFF -DTINYGLTF_BUILD_GL_EXAMPLES=OFF \ + -DTINYGLTF_BUILD_STB_IMAGE=ON -DCMAKE_INSTALL_PREFIX="${LOCAL_INSTALL}" + cmake --build "${WORK_ROOT}/tinygltf-build" --parallel + cmake --install "${WORK_ROOT}/tinygltf-build" + fi + + # build ktx + if [ ! -f "${LOCAL_INSTALL}/lib/cmake/KTX/KTXConfig.cmake" ]; then + git clone --depth 1 --branch v4.3.2 https://github.com/KhronosGroup/KTX-Software.git "${WORK_ROOT}/KTX-Software" + cmake -S "${WORK_ROOT}/KTX-Software" -B "${WORK_ROOT}/KTX-Software-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DKTX_FEATURE_TESTS=OFF -DKTX_FEATURE_TOOLS=OFF \ + -DKTX_FEATURE_VULKAN=ON -DCMAKE_INSTALL_PREFIX="${LOCAL_INSTALL}" \ + -DVulkan_INCLUDE_DIR="${Vulkan_INCLUDE_DIR}" + cmake --build "${WORK_ROOT}/KTX-Software-build" --parallel + cmake --install "${WORK_ROOT}/KTX-Software-build" + fi - name: Configure (Windows) if: runner.os == 'Windows' @@ -252,6 +356,8 @@ jobs: 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' @@ -273,10 +379,23 @@ jobs: -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;${VULKAN_SDK_SYSROOT}" \ "${extra_args[@]}" - name: Build run: cmake --build build --target SimpleEngine --parallel 4 + - name: Cache stats + shell: bash + run: | + if command -v ccache >/dev/null 2>&1; then + ccache -s + fi + if command -v sccache >/dev/null 2>&1; then + sccache -s + fi + - name: Test run: ctest --test-dir build --output-on-failure diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc index c11378cc..68913314 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc @@ -216,6 +216,6 @@ In this section, we've integrated our PBR implementation with the rest of the Vu This approach provides a solid foundation for rendering physically accurate materials, which we'll apply in the Loading_Models chapter when we load and render glTF models. It also gives us the flexibility to modify and extend the material properties as needed for our specific rendering requirements. -In the next section, we'll wrap up this chapter with a conclusion and discuss potential improvements and extensions to our lighting system. +In the next section, we'll explore how to add high-quality shadows using Vulkan Ray Query. -link:04_lighting_implementation.adoc[Previous: Lighting Implementation] | link:06_conclusion.adoc[Next: Conclusion] +link:04_lighting_implementation.adoc[Previous: Lighting Implementation] | link:07_shadows.adoc[Next: Shadows] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc index c3e237b4..3a98ddb1 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc @@ -1,10 +1,10 @@ = Conclusion -In this chapter, we've explored the fundamentals of lighting and materials in 3D rendering and introduced Physically Based Rendering (PBR) using the metallic-roughness workflow. We've covered the theory behind PBR and implemented a shader that can be used with glTF models. We've also learned how to use push constants to efficiently pass material properties to our shaders. +In this chapter, we've explored the fundamentals of lighting and materials in 3D rendering and introduced Physically Based Rendering (PBR) using the metallic-roughness workflow. We've covered the theory behind PBR, implemented a shader that can be used with glTF models, and added high-quality shadows using Vulkan Ray Query. We've also learned how to use push constants to efficiently pass material properties to our shaders. == What We've Learned -This chapter has taken you through the essential concepts needed to implement physically-based rendering in a Vulkan engine. We introduced the metallic‑roughness PBR workflow, mapped glTF material properties to shader inputs, and used push constants to drive per‑draw material parameters without descriptor churn. You saw how the BRDF pieces cooperate to conserve energy and produce plausible lighting, and how to plug the shader into a vk::raii‑based pipeline so models render correctly end‑to‑end. +This chapter has taken you through the essential concepts needed to implement physically-based rendering in a Vulkan engine. We introduced the metallic‑roughness PBR workflow, mapped glTF material properties to shader inputs, and used push constants to drive per‑draw material parameters without descriptor churn. You saw how the BRDF pieces cooperate to conserve energy and produce plausible lighting, and how to plug the shader into a vk::raii‑based pipeline so models render correctly end‑to‑end. Finally, we integrated hardware-accelerated ray-traced shadows for improved realism. == Making it click: a mental model of this PBR pipeline @@ -31,7 +31,7 @@ This mental model helps you predict how a change to any input will echo through == Potential Improvements -Our PBR pass is a solid baseline. The most impactful upgrades are image‑based lighting (environment maps for ambient/indirect), shadowing, and a few material extensions (e.g., clear coat or anisotropy). On the performance side, consider clustered forward or a deferred path when light counts grow. If you build an HDR chain, bloom and a more filmic tone mapper (ACES/Hable) round out the presentation. +Our PBR pass is a solid baseline. The most impactful upgrades are image‑based lighting (environment maps for ambient/indirect) and a few material extensions (e.g., clear coat or anisotropy). On the performance side, consider clustered forward or a deferred path when light counts grow. If you build an HDR chain, bloom and a more filmic tone mapper (ACES/Hable) round out the presentation. == Next Steps @@ -41,4 +41,4 @@ Remember that lighting is a complex topic with many approaches and techniques. T In the next chapter, we'll explore GUI implementation, which will allow us to create interactive user interfaces for our applications. -link:05_vulkan_integration.adoc[Previous: Vulkan Integration] | link:../GUI/01_introduction.adoc[Next: GUI - Introduction] +link:07_shadows.adoc[Previous: Shadows] | link:../GUI/01_introduction.adoc[Next: GUI - Introduction] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/07_shadows.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/07_shadows.adoc new file mode 100644 index 00000000..b1a03df1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/07_shadows.adoc @@ -0,0 +1,206 @@ += Shadows: Ray Query Integration + +Shadows are more than just dark patches on the ground; they are fundamental to how we perceive 3D space. They provide critical visual cues about the position, shape, and scale of objects, as well as the nature of the light sources illuminating them. In this section, we'll move from simple "flat" lighting to a more realistic model by implementing hardware-accelerated shadows using **Vulkan Ray Query**. + +== Understanding Shadows + +In the physical world, shadows occur when an opaque object obstructs the path of light from a source to a surface. To simulate this in computer graphics, we must solve the **visibility problem**: for any given point on a surface, is there an unobstructed line of sight to the light source? + +=== The Anatomy of a Shadow + +Real-world light sources are rarely infinitesimal points. Because lights have physical size (area lights), shadows often consist of two distinct regions: + +* **Umbra**: The darkest part of the shadow where the light source is completely occluded. +* **Penumbra**: The "soft" edge of the shadow where the light source is only partially occluded. + +While our initial implementation focuses on "hard" shadows (where a point is either 100% in shadow or 100% lit), the techniques we use here support advanced **soft shadowing** by sampling the light as an area rather than a point. + +=== Shadow Mapping vs. Ray Traced Shadows + +For decades, **Shadow Mapping** has been the industry standard. It involves rendering the scene's depth from the light's perspective into a texture, then comparing distances during the main render pass. However, shadow mapping comes with significant challenges: + +* **Resolution & Aliasing**: Shadows can look "blocky" if the shadow map resolution is too low. +* **Biasing Issues**: Finding the right "bias" to prevent *shadow acne* (self-shadowing) and *peter-panning* (shadows detaching from objects) is a constant struggle. +* **Memory Overhead**: Each light source requires its own depth texture. + +**Ray Tracing (Ray Query)** solves these issues by performing precise geometric intersections. Instead of checking a low-resolution texture, we ask the GPU: "Does this ray hit any triangle between point A and point B?" This results in pixel-perfect accuracy and simplifies the handling of multiple light types (point, spot, directional) without managing dozens of depth maps. + +== Ray Tracing Fundamentals + +To perform ray tracing efficiently, we can't just loop through every triangle in the scene for every pixel. Instead, we use **Acceleration Structures**. + +1. **Bottom-Level Acceleration Structure (BLAS)**: This stores the raw geometry (vertices and indices) for a single mesh. Think of it as a spatial index for a single object. +2. **Top-Level Acceleration Structure (TLAS)**: This contains *instances* of BLASs. Each instance has its own transformation matrix, allowing us to place the same mesh multiple times in the world with minimal memory overhead. + +=== Requirements and Setup + +Ray Query requires hardware support and specific Vulkan extensions. In our engine, we ensure these are enabled during device creation: + +* `VK_KHR_acceleration_structure` +* `VK_KHR_ray_query` + +=== Building Acceleration Structures + +In our engine, the `Renderer::buildAccelerationStructures` method in `renderer_ray_query.cpp` handles the creation. We build one BLAS for each unique mesh and then a TLAS that references them. + +[source,cpp] +---- +bool Renderer::buildAccelerationStructures(const std::vector &entities) +{ + // 1. Create BLAS for each unique mesh + for (auto &mesh : uniqueMeshes) { + buildBlas(mesh); // Compiles mesh data into a GPU-optimized format + } + + // 2. Create TLAS by instancing BLASs + std::vector instances; + for (auto &entity : entities) { + auto mesh = entity->getComponent(); + auto transform = entity->getComponent(); + + vk::AccelerationStructureInstanceKHR instance{}; + instance.transform = toVkTransform(transform->getMatrix()); + instance.accelerationStructureReference = getBufferDeviceAddress(mesh->blas.buffer); + instance.mask = 0xFF; // Allows filtering objects during ray tests + instances.push_back(instance); + } + buildTlas(instances); + + return true; +} +---- + +== Implementing Ray Query in Shaders + +With the TLAS built and bound to a descriptor set, we can perform visibility tests directly in our PBR fragment shader (`pbr.slang`). + +=== The Visibility Test + +We implement a helper function `traceShadowOccluded`. It initializes a `RayQuery` object, traces a ray, and checks if it hits any geometry before reaching the light. + +[source,slang] +---- +[[vk::binding(11, 0)]] RaytracingAccelerationStructure tlas; + +static const float RASTER_SHADOW_EPS = 0.002; + +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; + q.TraceRayInline( + tlas, + RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, // Optimization: any hit is enough to shadow + 0xFF, + ray + ); + + while (q.Proceed()) { + // q.Proceed() steps through potential hits. + // For simple opaque shadows, we don't need logic here. + } + + return (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT); +} +---- + +=== Integrating with PBR Lighting + +In the main lighting loop, we determine the occlusion status before adding a light's contribution. + +[source,slang] +---- +// Inside the fragment shader lighting loop +float3 L = normalize(light.position.xyz - input.WorldPos); +float distToLight = length(light.position.xyz - input.WorldPos); + +// Important: Move the origin slightly along the normal to prevent the ray +// from immediately hitting the surface it started from. +float3 shadowOrigin = input.WorldPos + N * RASTER_SHADOW_EPS; + +bool occluded = traceShadowOccluded(shadowOrigin, L, RASTER_SHADOW_EPS, distToLight); + +if (!occluded) { + // Add diffuse and specular contributions if not in shadow + directLighting += calculatePBR(L, V, N, ...); +} +---- + +== From Hard to Soft Shadows + +Our engine's Ray Query implementation in `ray_query.slang` goes beyond simple hard shadows by implementing **stochastic soft shadows**. Instead of treating the light as a single point, we treat it as an area light with a defined radius. + +=== Area Light Approximation + +We simulate an area light by jittering the light position for each shadow ray. Using a stable random number generator and disk sampling, we pick a random point within the "radius" of the light source. + +[source,slang] +---- +// Generate a random sample on a disk to simulate light area +float2 diskSample = rqSampleDisk(rngState); +float3 samplePos = lightPos + (T * diskSample.x + B * diskSample.y) * lightRadius; + +float3 L = normalize(samplePos - worldPos); +float distToLight = length(samplePos - worldPos); + +// Trace a ray toward the sampled point on the light +bool occluded = traceShadowOccluded(shadowOrigin, L, RASTER_SHADOW_EPS, distToLight); +---- + +=== Averaging Multiple Samples + +By tracing multiple rays (`shadowSampleCount`) toward different points on the area light and averaging the results, we produce a smooth transition between lit and shadowed regions (the penumbra). + +[source,slang] +---- +float visibilityAcc = 0.0; +for (int i = 0; i < shadowSampleCount; ++i) { + // ... calculate jittered L ... + visibilityAcc += traceShadowOccluded(...) ? 0.0 : 1.0; +} +float finalVisibility = visibilityAcc / float(shadowSampleCount); + +// Use visibility to scale the light's contribution +directLighting += calculatePBR(...) * finalVisibility; +---- + +== Challenges and Best Practices + +1. **Self-Shadowing (Acne)**: Even with ray tracing, floating-point precision can cause a ray to hit its own starting triangle. Always use a small `EPSILON` offset or a `TMin` value. +2. **Alpha Masking & Transmissivity**: For foliage or glass, a simple binary hit test isn't enough. Our engine handles this by: + * **Manual Alpha Testing**: In the `while(q.Proceed())` loop, we fetch the material's texture and discard hits that are transparent. + * **Transmissive Bypass**: We can flag certain materials (like glass) as non-occluding for shadow rays so they don't cast pitch-black shadows. +3. **Performance**: Ray tracing is expensive. While Ray Query is faster than a full ray tracing pipeline for simple visibility, it still adds cost. For high-performance scenarios, consider: + * **Denoising**: If you use multiple rays for soft shadows, you'll need a denoiser to clean up the grain. + * **Culling**: Don't trace rays for lights that are too far away or behind the surface. + +== Summary and Comparison + +Ray Query provides a powerful and flexible way to implement shadows in a modern engine. While it requires hardware support, it offers significant advantages over traditional shadow mapping: + +[cols="1,2,2"] +|=== +| Feature | Shadow Mapping | Ray Query + +| **Precision** | Limited by texture resolution (aliasing) | Pixel-perfect (geometric intersection) +| **Complexity** | High (biasing, multi-light management) | Low (direct visibility test) +| **Memory** | High (depth maps per light) | Low (acceleration structures) +| **Soft Shadows** | Complex (PCSS, blurring) | Native (area light sampling) +|=== + +== Next Steps & Further Reading + +Shadows are a deep topic. Now that you understand how to implement basic and soft shadows using Ray Query, you can explore more advanced areas: + +* **PCSS (Percentage Closer Soft Shadows)**: A raster-based technique for variable-penumbra shadows (where shadows get softer as the distance from the occluder increases). +* **Ambient Occlusion (RTAO)**: Use ray tracing to calculate how much ambient light reaches a point by tracing rays in a hemisphere around the normal. +* **Vulkan Ray Tracing Tutorial**: The link:https://nvpro-samples.github.io/vk_raytracing_tutorial_KHR/[NVIDIA Vulkan Ray Tracing Tutorial] is an excellent resource for deep-diving into these extensions. + +In the next chapter, we'll look at how to add a Graphical User Interface (GUI) to control these lighting and shadow parameters in real-time. + +link:06_conclusion.adoc[Previous: Conclusion] | link:../GUI/01_introduction.adoc[Next: GUI - Introduction] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc index 62d33ed2..9a8f152a 100644 --- a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc +++ b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc @@ -11,4 +11,5 @@ This chapter covers the implementation of basic lighting models and the use of p * link:03_push_constants.adoc[Push Constants] * link:04_lighting_implementation.adoc[Lighting Implementation] * link:05_vulkan_integration.adoc[Vulkan Integration] +* link:07_shadows.adoc[Shadows] * link:06_conclusion.adoc[Conclusion] From 361faea2a9aaff9be3a2446aeb919a1680e56b54 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 24 Dec 2025 01:01:37 -0800 Subject: [PATCH 22/24] Improve Windows CI: add Chocolatey bin path to GITHUB_PATH, enhance vcpkg cache handling, and update binary source configuration. --- .github/workflows/simple_engine_ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 4e1f7258..b0b61151 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -53,6 +53,10 @@ jobs: 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) @@ -287,10 +291,15 @@ jobs: 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=$env:RUNNER_TEMP\vcpkg-cache" | 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' From 9475cc51fb442f97bc16e2fb4bdb834cdf5178f6 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 24 Dec 2025 01:21:08 -0800 Subject: [PATCH 23/24] Improve CI workflows: add cleanup of partial/failed clones for tinygltf and KTX dependencies before retrying builds. --- .github/workflows/simple_engine_ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index b0b61151..986edcca 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -337,6 +337,8 @@ jobs: # build tinygltf if [ ! -f "${LOCAL_INSTALL}/lib/cmake/tinygltf/tinygltfConfig.cmake" ]; then + # Clean up any partial/failed clones before re-attempting + rm -rf "${WORK_ROOT}/tinygltf" "${WORK_ROOT}/tinygltf-build" git clone --depth 1 https://github.com/syoyo/tinygltf.git "${WORK_ROOT}/tinygltf" cmake -S "${WORK_ROOT}/tinygltf" -B "${WORK_ROOT}/tinygltf-build" -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ @@ -348,6 +350,8 @@ jobs: # build ktx if [ ! -f "${LOCAL_INSTALL}/lib/cmake/KTX/KTXConfig.cmake" ]; then + # Clean up any partial/failed clones before re-attempting + rm -rf "${WORK_ROOT}/KTX-Software" "${WORK_ROOT}/KTX-Software-build" git clone --depth 1 --branch v4.3.2 https://github.com/KhronosGroup/KTX-Software.git "${WORK_ROOT}/KTX-Software" cmake -S "${WORK_ROOT}/KTX-Software" -B "${WORK_ROOT}/KTX-Software-build" -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ From 66a00aeb3accfc09f0e254243376ab12afe66857 Mon Sep 17 00:00:00 2001 From: swinston Date: Wed, 24 Dec 2025 01:25:36 -0800 Subject: [PATCH 24/24] Refine Windows CI Vulkan SDK setup: improve caching logic, enhance diagnostics, and dynamically pass Vulkan paths to CMake configuration. --- .github/workflows/simple_engine_ci.yml | 73 ++++++++++++++++++-------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/.github/workflows/simple_engine_ci.yml b/.github/workflows/simple_engine_ci.yml index 986edcca..d4594658 100644 --- a/.github/workflows/simple_engine_ci.yml +++ b/.github/workflows/simple_engine_ci.yml @@ -84,27 +84,31 @@ jobs: key: ${{ runner.os }}-vulkan-sdk - name: Install Vulkan SDK (Windows) - if: runner.os == 'Windows' && steps.cache-vulkan-windows.outputs.cache-hit != 'true' + if: runner.os == 'Windows' shell: pwsh run: | $ErrorActionPreference = 'Stop' - if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - throw "Chocolatey is required on windows-latest runners" - } - - if (Test-Path "C:\VulkanSDK") { - Write-Host "Using existing Vulkan SDK at C:\VulkanSDK" + if ("${{ steps.cache-vulkan-windows.outputs.cache-hit }}" -ne "true") { + if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + throw "Chocolatey is required on windows-latest runners" + } + + if (Test-Path "C:\VulkanSDK") { + Write-Host "Using existing Vulkan SDK at C:\VulkanSDK" + } else { + Write-Host "Downloading Vulkan SDK installer..." + 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" + + Write-Host "Installing Vulkan SDK (silent, default feature set)..." + # NOTE: Do not pass --components here. LunarG has changed component IDs over time, + # and specifying them can cause 'Component(s) not found' failures. + Start-Process -FilePath $installer -ArgumentList "--accept-licenses --default-answer --confirm-command install" -Wait -NoNewWindow + } } else { - Write-Host "Downloading Vulkan SDK installer..." - 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" - - Write-Host "Installing Vulkan SDK (silent, default feature set)..." - # NOTE: Do not pass --components here. LunarG has changed component IDs over time, - # and specifying them can cause 'Component(s) not found' failures. - Start-Process -FilePath $installer -ArgumentList "--accept-licenses --default-answer --confirm-command install" -Wait -NoNewWindow + Write-Host "Vulkan SDK cache hit. Setting up environment..." } $vulkanPath = "" @@ -112,15 +116,29 @@ jobs: $vulkanPath = Get-ChildItem "C:\VulkanSDK" | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty FullName } if (-not $vulkanPath) { - throw "Vulkan SDK not found after install" + throw "Vulkan SDK not found after install/cache restore" } + Write-Host "Found Vulkan SDK at: $vulkanPath" "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 "Vulkan_INCLUDE_DIR=$vulkanPath\Include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "Vulkan_LIBRARY=$vulkanPath\Lib\vulkan-1.lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Vulkan SDK diagnostics (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Write-Host "VULKAN_SDK: $env:VULKAN_SDK" + Write-Host "Vulkan_INCLUDE_DIR: $env:Vulkan_INCLUDE_DIR" + Write-Host "Vulkan_LIBRARY: $env:Vulkan_LIBRARY" + if (Test-Path "$env:Vulkan_INCLUDE_DIR\vulkan\vulkan.hpp") { + Write-Host "vulkan.hpp found" + } else { + Write-Warning "vulkan.hpp NOT found at $env:Vulkan_INCLUDE_DIR\vulkan\vulkan.hpp" + } + - name: Cache Vulkan SDK (Linux) if: runner.os == 'Linux' id: cache-vulkan-linux @@ -365,12 +383,21 @@ jobs: - name: Configure (Windows) if: runner.os == 'Windows' shell: pwsh - 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 + run: | + $extraArgs = @() + if ($env:Vulkan_INCLUDE_DIR) { + $extraArgs += "-DVulkan_INCLUDE_DIR=$env:Vulkan_INCLUDE_DIR" + } + if ($env:Vulkan_LIBRARY) { + $extraArgs += "-DVulkan_LIBRARY=$env:Vulkan_LIBRARY" + } + + 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 ` + $extraArgs - name: Configure (Linux) if: runner.os == 'Linux'